Rookie Challenge Unintended Solution

Nov. 2, 2020 // echel0n

Rookie



We played STMCTF'20 last weekend. In my opinion, this was the least enjoyable STMCTF. There were nice challenges but the overall quality was mehhhhhhhhh. Anyway, This is a writeup of third challenge of Reverse Engineering category. This is not an intended solution. I learnt this after got the flag.

The Action

The given files are private key and rookie-server executable.

  1. $ file rookie-server
  2. rookie-server: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=5b25dd40514f0f9b9f0beb41df1dfb8d5d1b9e48, for GNU/Linux 3.2.0, stripped

@icecream coded a client for this server, this is the client code.

  1. import os
  2. import sys
  3. with open("channel", "wb") as channel:
  4. channel.write(sys.stdin.buffer.read(512))
  5. os.system("openssl rsautl -decrypt -in channel -inkey ./private.pem")
  6. while True:
  7. with open("channel", "wb") as channel:
  8. channel.write(sys.stdin.buffer.read(1024))
  9. os.system("openssl rsautl -decrypt -in channel -inkey ./private.pem")

You can use this python code with this line below.

  1. nc 10.0.0.0 1337 | python client.py

I won't explain steps of reversing this executable, I will show you the code.

You should see something like this.


Umm WHAT?

At this point, I started to feel that this challenge environment is really messed up. (Custom environment variables, custom privilege implementation for clients etc.)

Also the source code is in src/ we can use grep command to get small pieces of source code. Examples are;

  1. #include <fstream>
  2. #include "auth.h"
  3. #include <grp.h>
  4. //if session uid is 0, elevate, read the flag with 600:root permissions, and output it
  5. int yield_flag(session s){
  6. if(s.uid) return 1;
  7. setuid(0);
  8. std::ifstream flag("flag.txt");
  9. char buf[59];
  10. if (flag.is_open()){
  11. flag.read(buf,58);
  12. buf[58]=0;
  13. outputEncryptedString(buf);
  14. }
  15. flag.close();
  16. //exit(1337);
  17. }
  18. int authenticate(session s, char* password){
  19. std::string pwd(password);
  20. std::string hash = sha512(pwd);
  21. //outputEncryptedString((char*)hash.c_str());
  22. if(!s.uid || !hash.compare("8b586bf1b19e2d37191f780286c5ff74f7134ab650aebf7b6226d0d03b5d23acc0f443445315fb9f89ce2322fe1499d1124ca22f0be158ad7a8bb9ea549bb1ad")){
  23. outputEncryptedString("Authenticated\n");
  24. s.uid=0;
  25. setuid(0);
  26. yield_flag(s);
  27. }else{
  28. //Seriously.
  29. outputEncryptedString("Thats sha512. Its 17 characters [!-~]. Please don't waste your time bruteforcing. You can't.\n");
  30. }
  31. }
  32. int dropPrivs(){
  33. /*
  34. Set uid and gid to 1000, dropping root privileges.
  35. */
  36. /* If root privileges are to be dropped, be sure to pare down the ancillary
  37. * groups for the process before doing anything else because the setgroups( )
  38. * system call requires root privileges. Drop ancillary groups regardless of
  39. * whether privileges are being dropped temporarily or permanently.
  40. */
  41. gid_t guid = 1000;
  42. setgroups(1, &guid);
  43. setgid(guid);
  44. setuid(0);
  45. seteuid(1000);
  46. }
  1. int builtin_exit(session s, char ** args) {
  2. //...
  3. //check against hash
  4. return 0;
  5. }
  6. int builtin_cd(session s, char ** args) {
  7. if (args[1] == NULL) {
  8. outputEncryptedString((char*)"No arguement to \"cd\"\r\n");
  9. } else {
  10. if (chdir(args[1]) != 0) {
  11. outputEncryptedString((char*)"No such directory\n");
  12. }else{
  13. outputEncryptedString((char*)" \n");
  14. }
  15. }
  16. return 1;
  17. }
  18. int builtin_flag(session s, char ** args) {
  19. if(s.uid)
  20. outputEncryptedString((char*)"STMCTF{You need to elevate for flag}\r\n");
  21. else yield_flag(s);
  22. }
  23. int builtin_authenticate(session s, char ** args) {
  24. char* passwd = args[1];
  25. authenticate(s,passwd);
  26. }
  27. int builtin_cid(session s, char ** args) {
  28. char* passwd = args[1];
  29. sprintf (buffer, "connection id:%d\nCurrent uid:%d\nsocket fd:%d\n", s.connectId, s.uid, s.sockfd);
  30. outputEncryptedString(buffer);
  31. }
  32. int builtin_sudo(session s, char ** args) {
  33. char* command = args[1];
  34. if(args[0]==0) return 1;
  35. if(s.uid==0 && !strcmp(args[1],"cid")){
  36. builtin_cid(s, &args[1]);
  37. }
  38. else if(s.uid==0 && !strcmp(args[1],"stm")){
  39. builtin_flag(s, &args[1]);
  40. }
  41. else if(s.uid==0 && !strcmp(args[1],"cd")){
  42. builtin_flag(s, &args[1]);
  43. }
  44. else if(s.uid=0 && !strcmp(args[1],"exit")){
  45. outputEncryptedString("Not Allowed\n");
  46. }
  47. }
  48. int builtin_rf(session s, char ** args) {
  49. setuid(1000);
  50. std::ifstream fs(args[1]);
  51. char buf[512];
  52. if (fs.is_open()){
  53. while(!fs.eof()){
  54. fs.read(buf,512);
  55. std::cout << buf;
  56. }
  57. }
  58. fs.close();
  59. }
  60. int (*builtin_func[])(session, char **) = {&builtin_cd, &builtin_cid, &builtin_sudo, &builtin_exit, &builtin_authenticate, &builtin_flag, &builtin_rf};
  61. int numBuiltins = 7;
  62. const char *builtin_str[] = {
  63. "cd",
  64. "cid",
  65. "sudo",
  66. "exit_BIND",
  67. "authenticate",
  68. "stm",
  69. "readfileplain"
  70. };

Main code is...

  1. int main(int argc, char *argv[]){
  2. #ifdef DEBUG
  3. std::cerr << "<WARNING> Running with debug enabled\n";
  4. #endif
  5. //register sigint handler
  6. signal(SIGINT, signalHandler); signal(SIGABRT, signalHandler); signal(SIGTERM, signalHandler);
  7. signal(SIGCHLD, SIG_IGN);
  8. char **args;
  9. int status;
  10. std::string str;
  11. char userbuf[64];
  12. char hostbuf[64];
  13. char cwd[128];
  14. int portno;
  15. socklen_t clilen;
  16. struct sockaddr_in serv_addr, cli_addr;
  17. int n;
  18. if(argc!=2){
  19. portno = 4444;
  20. if(argc!=2){
  21. portno = 4444;
  22. return 1;
  23. }else{
  24. portno = atoi(argv[1]);
  25. }
  26. //create file descriptor
  27. sockfd = socket(AF_INET, SOCK_STREAM, 0);
  28. if (sockfd < 0) return 2;
  29. serv_addr.sin_family = AF_INET;
  30. serv_addr.sin_addr.s_addr = INADDR_ANY;
  31. serv_addr.sin_port = htons(portno);
  32. serv_addr.sin_port = htons(portno);
  33. clilen = sizeof(cli_addr);
  34. int connectionID = 0;
  35. //bind to tcp port
  36. if (bind(sockfd, (struct sockaddr *) &serv_addr,sizeof(serv_addr)) < 0)
  37. return 3;
  38. listen(sockfd,5);
  39. int newsockfd;
  40. while(newsockfd = accept(sockfd,(struct sockaddr *) &cli_addr,&clilen)){
  41. char cliIP[16];
  42. std::cout << "Accepted connection from " << inet_ntoa(cli_addr.sin_addr) << "\n";
  43. session s;
  44. s.connectId=connectionID++;
  45. s.sockfd=newsockfd;
  46. s.uid=1000;
  47. if(!pid){
  48. initSession(s, newsockfd, serv_addr);
  49. }
  50. //close only for parent so we don't exceed the fd limit 1024. Socket is alive for child fork.
  51. close(newsockfd);
  52. }
  53. }

It goes like this, we got what we have to know from here.
Actually we have root privileges but the server is telling lies about our UID.
Also, it seems, the intended way to solve this challenge running stm command.

WE ARE ALREADY ROOT, WHY WE HAVE TO CHASE AFTER THIS STM COMMAND ????

Yeah, why?
We just have to bypass argc checks and we can run actual 'bash' commands by root.

And the answer was the 'bash' command itself.

The flag is STMCTF{dOub1E_sUdo_t0_b4ckd00r_0r_ju5t_a_r0okIe_M1st4ke?}
At this point, I was like 'What, which double sudo?'. I thought that was the intended solution.
Real solution was 'sudo sudo stm'.


jokes on you challenge author, maybe this was the 'rookie' mistake :P (jk)


Thanks for reading, I hope you enjoyed!